For contributors

The two orderings a net carries

rcsr_index is where a net is shown; complexity_rank is when it was ingested. They are different columns and conflating them is the known failure mode.

Every ingested net carries two numbers that both look like "position in a list", and they answer two unrelated questions:

  • rcsr_index — where does this net belong in the RCSR symbol ordering a reader expects to browse? A presentation order.
  • complexity_rank — in what order were the nets processed at ingest, simplest first? A processing order. A listing will sort by it on request, but it is never a claim about the net itself.

They are not correlated and they are not interchangeable. pcu carries complexity_rank 1 — one vertex, three edges, the simplest thing in the set — and rcsr_index 1536, because p is a long way into the alphabet. aab is the mirror image: rcsr_index 1, complexity_rank 2131. A listing can be asked to sort by either (see below); what neither number will survive is being read as the other. Reporting a complexity_rank as a net's position in the RCSR index, or an rcsr_index as a statement about how complicated a net is, is the mistake this page exists to stop.

Both numbers are now on the pcu row. They were not always: pcu is one of the five seeded demonstration rows the loader will not overwrite, and it used to be skipped entirely, which left it with NULL in both columns. It is now enriched instead — the ingest writes the two orderings and their provenance onto a pre-existing row and touches nothing else — so a page describing pcu as an example of a blank index is describing a state that no longer exists.

The licence position, which is now split

The dominant fact about this collection, stated before anything below depends on it. There are 3077 net rows and all of them are served, under two different licences. The split is not arbitrary and it is not about the nets — it is about who owns the source the values came from:

Rows Source licence licence_source published
2292 rcsr+mofplus CC-BY-4.0 the project decision, quoted below true
146 mofplus CC-BY-4.0 the project decision, quoted below true
634 rcsr RCSR-2019 the RCSR agreement (© 2019 RCSR.net) true
5 NULL (seeded) CC-BY-4.0 NULL true

A record at least one of whose values came from MOF+ is the project's own data. MOF+ is this group's own service, so those nets are ours to license, and the project licenses its data CC BY 4.0. That is a decision recorded in CONTEXT §6, not a grant discovered on disk, and the row says so in its own licence_source:

MOF++ project decision, CONTEXT §6 (Licensing): MOF+ is this group's own service, so nets retrieved from it are the project's own data to license, and the project licenses its data CC BY 4.0. No third-party grant is claimed.

The last sentence is load-bearing. The MOF+ reader's own finding — that no machine-readable licence grant is stated anywhere in the client or the retrieval cache — is still true and is still carried in the ingest report beside the decision. The decision answers a different question from the one that note asks.

A record sourced only from the RCSR archive is somebody else's data, and it is served on somebody else's terms. rcsr.anu.edu.au is a third party and the archive file states no terms, so those 634 rows were held unpublished — 403 at their own URIs — until 2026-09-09, when the repository owner confirmed the terms: the nets are © 2019 RCSR.net and MOF++ redistributes them under a private agreement with the rights holder. licence holds RCSR-2019, which is deliberately not a Creative Commons identifier, because the agreement permits redistribution by MOF++ and grants a reader no onward licence; the identifier carries open: false and the record page prints "Not an open licence". The project's own CC BY 4.0 decision covers the project's own data and still says nothing about somebody else's archive. The resolution and what it does not cover are in docs/reports/net-ingest-2026-09-09.md §3 in the repository.

The held state itself remains, and is still tested, for the next deposit whose provenance the licence decision does not recognise. A held record is not hidden and not a 404. It is absent from every listing, and its own URI answers 403 with its identity, its provenance and both of these orderings intact, plus the list of what is withheld (nets.held_dict, and web.held_record for why that status code). Its field_sources is filtered as well: an attribution that names a source is served, a sentence that states what a value is — the space group spelled out in a handedness note, the catenation in a heuristic check — is withheld with the value, and field_sources_withheld names what was dropped. The client-side detail is in the API reference; the position in Licensing.

One wart, recorded rather than smoothed over: the five seeded demonstration rows are published as CC-BY-4.0 with licence_source NULL. That is the seed's own position, from before any of this existed. The licence pass reads only rows with a non-null source, so it never reaches them — deliberately, because it must not relicense a row whose values it did not write. They are five published rows whose licence names no basis, and the edit that would fix it is in seed.py, not in the loader.

Presentation order — rcsr_index

This is the column anything that lists nets in RCSR order reads. It is persisted at ingest and never recomputed downstream: no request path, no template and no export re-derives it. A page that wants RCSR order does ORDER BY rcsr_index and a net whose column is NULL sorts after the block (see below).

Ascending rcsr_index is what a net listing does by default — the HTML search at /data/nets and the REST listing GET /api/v1/nets alike. There is exactly one ORDER BY for a list of nets, nets.order_clause in services/nets.py, and every surface calls it, so they cannot come to disagree.

The three orderings a listing can be asked for

nets.ORDERINGS is ("rcsr", "name", "complexity") and nets.DEFAULT_ORDER is rcsr. A caller chooses with order= — the Order select on the search form (templates/nets_search.html, read by web.nets_page) and the order query parameter on the REST listing (rest.search_nets).

order ORDER BY
rcsr (default) rcsr_index ascending, then name
name name ascending
complexity complexity_rank ascending, then name

Three properties of that clause are deliberate and worth knowing before you depend on them:

  • Nulls are pinned, not left to the database. SQLite sorts NULLs first and PostgreSQL sorts them last, so the clause states the placement itself: indexed nets first in index order, then unindexed nets by name. The same is done for a missing complexity_rank.
  • Name is the tie-break inside every ordering, so pagination is stable: a non-deterministic tail silently duplicates and drops rows between pages.
  • An unrecognised order= is not an error. nets.parse_order returns the default, and the surface names the ordering it actually used back to the reader — in the results sentence on the page and in the order field of the JSON envelope.

The coordination-sequence search is the exception: nets.search_cs takes no order and always calls order_clause() with its default, so the select on the form does not apply to a cs query.

It is derived, not authoritative

This is the part that must not be forgotten. rcsr_index is our arithmetic, not RCSR's. The rule, quoted from mofpp.services.rcsr.RCSR_INDEX_RULE, which is where it is defined once so that this page can quote it rather than restate it and drift:

rcsr_index is derived, not authoritative: the archive carries no ordinal. Every symbol in the archive is sorted into ascending plain ASCII lexicographic order and given its 1-based position in that ordering. Because - (0x2D) sorts before every letter, a suffixed variant falls immediately after its base symbol and before the next base (pcu < pcu-b < pcv). The provenance recorded on every record is derived.

Concretely, over the 2930 symbols in RCSRnets-2019-06-01.arc: pcn is 1535, pcu is 1536, then pcu-g 1537, pcu-g-e 1538, pcu-h 1539. Variants group under their base for free, which is the behaviour a reader browsing an index wants, and it falls out of plain byte comparison without any suffix handling.

Every record read from the archive has rcsr_index_provenance set to the string derived (rcsr.RCSR_INDEX_PROVENANCE). That column exists precisely so nobody has to guess later whether the number was read or computed. In the database that is 2930 rows — every symbol in the archive, with none left behind: the four seeded rows whose names are archive symbols (pcu, pyr, qom, tbo) are enriched with the index rather than skipped. The other 147 rows carry NULL: the 146 ingested MOF+-only nets and one seeded row (pcu-c), none of which has a symbol to derive an index from.

The value is served as well as stored: every net representation carries rcsr_index_provenance and a sentence spelling out what the number is (nets.RCSR_INDEX_NOTES), and the same sentence is a template global, so the page and the JSON cannot come to describe it differently.

A blank index is not one fact but three, and the served record says which: alongside a null rcsr_index goes an rcsr_index_absence of

  • outside-archive — the name is not one of the 2930 symbols, so there is no position to hold. All 147 blanks in the database today are this, raf and ycs-z among them.
  • unrecorded — the name is an archive symbol, so the net does have a place in the ordering and this row simply carries no number. No row is in this state. It was the state of the four seeded archive symbols before enrichment, and it stays a defined answer because a future load that skips a row would put one back into it.
  • unknown — the local archive could not be read, so the two cannot be told apart.

None of them is a licence to derive an index at request time; the column stays the only authority for the number and nets.rcsr_index_absence only explains its absence.

Why it had to be derived

The local Systre archive stores no ordinal, and its physical block order is not the order to use. The first blocks in the file are mcm, tts, tth, sql, hxl, hcb, kgm, fsz — neither alphabetical nor RCSR's published listing. That is molsys archive accretion order: the sequence in which nets happened to be added to the file. Presenting nets in it would be presenting an artefact of somebody else's file history as though it were a chemical index.

RCSR's own listing order is not on this machine either. There is no 3dall.txt, no .cgd dump and no other RCSR export here — only the .arc, which is a canonical-key lookup table carrying key, version, id, checksum and two empty fields, and no cell, coordinates, spacegroup, transitivity, coordination sequence or vertex symbol. So the choice was between a documented derivation and no ordering at all, and the derivation was taken with its provenance stamped on every row.

What a future contributor should do

If you obtain RCSR's real published listing order — from an upstream export, a 3dall.txt, or the site itself — replace this column with it and flip rcsr_index_provenance from derived to authoritative. That is the intended end state. Nothing downstream computes the index, so replacing the column is a data migration and not a code change; the provenance string is how a reader tells which one they are looking at. Until then, do not cite rcsr_index as though it were RCSR's numbering, because it is not.

Nets with no RCSR symbol

A net that exists only in the MOF+ source has no symbol in the archive, so it gets rcsr_index NULL — never 0, never an invented tail number — and sorts after the whole RCSR block. The order within that tail is quoted here verbatim from mofpp.services.reconcile.SECONDARY_ORDER_RULE:

A net with no RCSR symbol has rcsr_index NULL and is placed after the whole RCSR block. Within that tail, records are ordered by ascending plain ASCII-lexicographic net name — the same byte-for-byte, case-sensitive comparison rcsr.sort_symbols applies to the archive symbols, with no case folding, normalisation or suffix handling. Names are unique across the union, so the order is total, and it depends on nothing but the names themselves: it is identical across runs, processes and platforms.

147 nets are in that tail today, not the fifteen a sampled cache once showed. It opens acs-b, afw-c, ant-c, bad, bae and closes xfv-z, ycr-z, ycs-z, yct-z, yct-zs. They are the MOF+-only nets: MOF+ deposit names such as eea_vc3_1-1_A and sln_112_topoFF-sq-trig that were never going to be RCSR symbols, and suffixed variants such as dia-c6 and srs-c4 that the 2019 archive does not carry.

Ingestion order — complexity_rank

complexity_rank says which nets to ingest, reconcile and check first, on the principle that the simplest ones fail fastest and most legibly: if the loader is broken, you want to find out on a one-vertex net, not two hours in on a net with 3264 vertices. It is a processing order, and that is the whole of its meaning.

A listing will sort by it if a reader asks for order=complexity, which is a genuinely useful way to see the small nets first. That does not promote it into a statement about a net: it is not a difficulty score, not a curation or quality ranking, and rank 1 does not mean "most important". Anything that reads it as an assessment of the net rather than of the ingest run is reading it wrong, which is why the results sentence on the page that offers the ordering calls it ingestion complexity rather than complexity.

The key is a chain of levels and the first available level wins:

  1. transitivity — the quadruple pqrs. A regular net (1 1 1 1) is the simplest thing there is, so this level dominates whenever it is known.
  2. vertex_and_edge_classes — number of distinct vertices, then number of distinct edges. (These are p and q by another name: a caller that holds only the first two members of the quadruple should pass them here rather than pretend to a full transitivity.)
  3. cell_contents — number of vertices in the unit cell, then edge count.
  4. max_coordination — the largest coordination number in the net.
  5. The symbol, lexicographically — the deterministic final tie-break. This is not a level in the sense the other four are. A level is a claim about how complicated a net is; a symbol is not. It is appended to the key produced at every level, so two nets a level cannot separate are still totally ordered, and it is therefore never "first available" on its own.

A level is available only when every one of its values is present. Level 3 with an unknown edge count would have to compare a known vertex count against nothing, and inventing the missing half is exactly the fabrication this data set cannot afford. A half-known level is skipped, not patched.

Every rank carries a complexity_basis: the level that was used, the values it was computed from, and the resulting key. A rank can therefore be audited from the stored row alone, without re-reading either source or re-running the ranking.

Levels 1, 2 and 4 are used by nothing in this collection, and that is a fact about the sources rather than about the ranking: no source here supplies a transitivity, and every record that supplies a coordination number also supplies the cell contents that level 3 asks for first.

That stays true of the ranking even though p and q are now computed and stored. complexity_rank is a processing order fixed at ingest and nothing downstream recomputes it, so it keeps describing what was known when the nets were loaded. A future ingest run on a collection that already carries computed transitivities could reach level 2 — but it would be a new ranking, written with its own complexity_basis, not a silent revision of this one.

Nets with no key at all

A net for which levels 1 to 4 are all unavailable has no key. It is ranked last and counted — never ordered by its name as though its name said something about its complexity, and never given a guessed level.

Of the 3077 nets in the union, 2443 rank at level 3 (cell_contents) and 634 are unkeyed — the RCSR-only block, and all of it, because the archive supplies no transitivity, no cell contents and no coordination number. The keyed nets hold ranks 1 to 2443 and the unkeyed hold 2444 to 3077, in the tie-break order, and the count is reported rather than absorbed.

Which source supplies what

Two sources, and each is the only one on this machine that measures what it owns. The archive is the canonical-key authority and carries no crystallography at all; the .mfpx deposit is the only source of a cell, a coordinate or an edge. Every row records this per field in field_sources, so the table below is a summary of what is on the rows, not a substitute for reading them.

Field Source
systre_key, its checksum RCSR archive, carried verbatim — this column holds archive text and nothing writes into it but the archive. A key is computed here, from the MOF+ deposit, and is served under topology.symmetry rather than merged into this column
rcsr_index, rcsr_index_provenance Derived from the archive symbol list (above)
source, source_id, retrieved Whichever source(s) the record came from
cell parameters cella…cellgamma MOF+ .mfpx deposit
natoms MOF+ .mfpx deposit
catenated At ingest, a heuristic on the name, attributed mofplus:name-heuristic. Since 2026-09-12 the column holds a value computed from the graph on 3071 rows, with the heuristic's verdict kept beside it — see below
vertices, edges and their periodic images MOF+ .mfpx deposit, parsed at ingest
cs (per vertex class) MOF+ get_cs, as stored — never walked or recomputed; only its class assignment is checked, see below
vs (per vertex class) MOF+ get_vs, as stored, truncation and all. On six rows of one record (dum) a settled curation decision presents the computed symbol instead and serves the stored string verbatim beside it; the column itself is untouched — see Curation calls, below
complexity_rank, complexity_basis Computed at ingest from whichever of the above are present
topofile The deposited .mfpx, written verbatim from the retrieval cache
licence, licence_source, published The project's own decision, by source (above) — never read from either source

Where both sources offer the same field with different values, identity and canonical-key fields go to RCSR and crystallographic and geometric fields go to MOF+; the losing value is kept in the reconciler's report and named in field_sources, never merged into the winner. A conflict on a field with no agreed authority is refused rather than resolved by a default. On this data no conflict arises at all: the two offerings overlap only on source_id, which is equal by construction for a matched net.

The Systre key, and why it is the whole of an RCSR-only record

systre_key and systre_key_checksum are on 2930 rows — every archive symbol — carried verbatim from the .arc and attributed to rcsr in field_sources. They are NULL on the 147 rows with no RCSR symbol: an unknown key, not an empty one. This column is archive text, and nothing writes into it but the archive. The checksum stored beside it is the archive's own, copied from the same block so that the copy can be audited against the upstream file later; it is not computed or checked here.

That is a rule about the column, not a statement that no key can be computed. Systre 19.6.0 is installed (var/tools/Systre-19.6.0.jar, checksum verified before every run), and a canonical key is computed here — from the MOF+ deposit, which is an independent graph, and served under the record's topology.symmetry block with the basis it came from. It is deliberately not written into systre_key: a computed key is our statement and the column is RCSR's, and merging them would lose exactly the distinction that makes the comparison worth anything.

The comparison is the point. 2296 records carry both a deposit and an archive key. For 2292 of them the key computed from the deposit equals the archive key character for character, so the two sources really do hold the same net under that name. For one, skl-a, it does not: the MOF+ deposit under that name is a different net from RCSR skl-a (120 edges in the minimal repeat unit against 124, different degrees, TD10 1195.29 against 745.71, genus 65 against 69), and that row records conflicting-systre-key rather than either source being edited to agree.

That disagreement is one of the two in this collection that have been ruled on. On 2026-09-12 the repository owner decided that the identity of the symbol stays with the RCSR archive key and that the deposited topology is a different net, not served as the topology of this symbol. The consequence is a rule in the apply step rather than an edit: where name_match is conflicting-systre-key, no column is written from the deposit basis, the archive-key basis supplies them, and a value already written from the blocked basis is cleared. On this row the five computed columns keep the numbers they had — P42/nmc, 137, p 5, q 11, not catenated, which Systre gives for both graphs — and changed only their attribution, from computed:… (mofplus-deposit) to computed:… (rcsr-archive-key). That was the whole of the defect: properties measured from the deposited file were being presented on a record whose identity is RCSR's symbol. Both graphs are still served in full, each under its own basis, and the ruling, its evidence and its two follow-ups at the sources are served with the record as a curation call (below). The rule, the evidence and the row it moved are in docs/reports/net-analysis-2026-09-12.md §12 in the repository.

A further 3 are uncorroborable because Systre timed out on their deposits — unchecked, which is not the same as mismatched. 116 MOF+-only records carry a rcsr_identification note saying which RCSR net Systre recognises their topology as; none of them gains an archive key from it. 2292 + 1 + 3 = 2296: every both-source record is accounted for.

The key is now served, as a systre block on the full record and on the HTML page (nets.systre), carrying the key, the checksum, the provenance of each, and — where the key is blank — an absence of outside-archive with a sentence saying that neither source we read states a key for that net. List rows do not carry it: a 500-row page of Systre keys is not a listing.

On an RCSR-only record it is the only substantive datum there is. Such a row has no cell, no vertices, no coordination sequence and no catenation, because the archive carries none of those; what it has is a canonical key, a checksum, a derived index and a name. That was also the sharp edge of the licence position while those records were held: the one thing they hold was the one thing withheld, and withheld for the right reason, since the key is RCSR archive text and the RCSR archive's terms were exactly what nobody had confirmed. The terms were confirmed on 2026-09-09 and the key is served; the held representation still says this in its own systre block rather than staying silent about it, and systre_key is still named in a held record's withheld list.

Coordination sequences are served with their provenance

The same discipline, applied to the sequences: a full record carries a coordination_sequences block whose state says how far the values below it can be trusted — corroborated (2412 records), reassigned (7), not-recorded (653), unattributed (5, the seeded rows), and two states nothing is in today, uncorroborated and withheld. Without it a reassigned record and a corroborated one look identical, which asserts that the source supplied an assignment it did not supply. What the states mean is the subject of the next section.

catenated is three-valued, and NULL means unknown

catenated is nullable, and the three states are true, false and NULL — not two. NULL is unknown, never "no". The column is nullable precisely so that absence can be stated instead of guessed, and a listing asked for include_catenated=false counts the rows it could not certify separately rather than returning them as un-catenated.

It is now a measurement rather than a name heuristic. Interpenetration is counted from the quotient graph — the lattice generated by its cycle voltages, and how many copies of the net that lattice unfolds into — for every graph the collection holds, on either basis. In the database today: 54 true, 3023 false, 0 NULL, of which 3071 values carry computed:mofpp.periodicity-v<VERSION> (<basis>) in field_sources, one is the seeded pcu-c row, and five keep the old heuristic value because the computation deliberately declines to decide.

Those five are the reason the three states still matter. sql-c, sql-c6, hcb-c and hcb-c3 are stacks of 2-periodic layers and lcw is three sets of 1-periodic chains; whether layers are polycatenated is a question about how they thread through space, which a quotient graph does not encode. The analysis returns None with that reason, and the apply step writes nothing rather than resolving it.

What the sources said is kept, not discarded. The old value came from the legacy client's rule — the name contains -c — and is preserved verbatim as field_sources["catenated_name_heuristic"] on 2433 rows, reading "<Yes|No>; agrees|disagrees with the computed value". It disagrees on 8: qtz-c, qtz-c3, qtz-c4, qtz-c6, dia-c3s, eta-c, eta-c4 and twt-c are each a single copy of the parent net, not an interpenetrated array — each deposit is edge-for-edge the parent's own (or, for dia-c3s, a different cell of dia), and Systre reads each as one connected net. Nothing was found in the other direction. A sentence qualifying whichever state a row is in (nets.catenated_note) still travels with the value everywhere it is shown, because false on its own reads as a finding.

Where the name and the measurement disagree, the record says so in one more place than a cross-check would, which is the subject of the next section.

Curation calls: where a source and the graph disagree

A cross-check reports a comparison and stops — corroborated, contradicted, not comparable. Some disagreements need one more thing said about them, because they are about the record rather than about the computation: on the eight -c records above, neither value is a bug. The marker is what the source named the record, the count is what the source deposited, they cannot both describe one object, and no amount of graph theory says which the source meant.

A curation call is that statement. Every call names the claim and what the claim rests on, the finding with the tool, the version and the basis it was computed on, the evidence a reader can check, what the record's columns hold today, the decision somebody has to make — and that nothing was changed pending it. Three properties hold for every call:

  • Derived when the record is served, and stored nowhere. A call is assembled from field_sources and the analysis rows the record already carries. No column is written for it, no list of net names exists anywhere to go stale, and a record that stops meeting a rule stops carrying the call at its next request.
  • It quotes; it does not re-judge. The claim comes from field_sources and the finding from a stored analysis row, each with its tool, version and basis. Nothing is recomputed and neither side is preferred.
  • An open call and a settled one are the same object. state is open or settled; a settled call adds ruling, taken_by, taken_on and consequence, and the question and both readings stay stated either way.

10 of the 3077 records raise a call today: the eight -c records above, open, kind catenation; dum, settled, kind vertex-symbol; and skl-a, settled, kind identity. Every other record's list is empty, which says that no rule fired — on an unanalysed record that is mostly a statement about the analysis, not a clean bill of health.

A ruling lives in one place and is keyed to the question it answered. The register is nets.CURATION_DECISIONS, and an entry matches a record only when the kind, the basis, the tool, the exact pair of strings in dispute and — where the entry names one — the checksum of the graph that was analysed are all the ones that were ruled on. No net name appears in it. A different record showing a different disagreement gets an open call rather than inheriting somebody else's ruling, and so does this record after a new deposit or a changed archive key, because the ruling was made about what was there.

What a decision changes is what this site presents, never what is stored. Both rulings were taken by the repository owner on 2026-09-12, and neither wrote anything:

  • dum — six of its twelve vertex rows store 3.8(4).8(4) where mofpp.rings computes 3.8(2).8(2) from the deposited graph, while the record's other six rows are reproduced exactly by the same tool in the same convention. Ruled: the stored symbol is wrong; the computed symbol stands. Those six rows now present the computed symbol as their vertex symbol, the stored string is served verbatim beside it under vertex_symbol_as_stored with a sentence naming the decision, and a note above the vertex table says so once for the record. Vertex.vs still holds the source's string, and the value at MOF+ is untouched.
  • skl-a — the identity conflict described above. Ruled: the identity of the symbol stays with the RCSR archive key; the deposited topology is a different net and is not served as the topology of this symbol. Two follow-ups are part of the ruling and neither is a local edit — MOF+ is to be asked to re-deposit or rename the record, and the missing loop-derived edges are to be reported to RCSR. This project does not correct another resource's data inside its own copy.

A held record does not carry its calls: curation is in the withheld list for the same reason topology is, because a call spells the measured values out in words. The served shape, field by field, is in the API reference.

Coordination sequences and the class-mapping hazard

The coordination sequences are the source's own, byte for byte. Nothing here computes one into a record, extends it, repairs it, or replaces it with a walked one. But which vertex class each supplied sequence describes is a separate question from what the sequences are, and getting it wrong serves a reader a real sequence attached to the wrong vertex — which is worse than serving nothing, because it looks right.

The cheap invariant is cs[k][0] == coordination_number: the first term of a coordination sequence is the coordination number of the vertex it describes. It fires first and it refuses outright any record whose sequences cannot belong to the classes they are stored against.

That check is blind whenever two vertex classes share a coordination number. If classes 0 and 1 are both 3-coordinated, a sequence swapped between them still satisfies cs[k][0] == coordination_number on both, and nothing in the arithmetic notices. Measured over the complete cache: 5537 vertex classes across 2443 records, and 1079 of those records — 44% — have two or more classes of equal coordination number, so on nearly half the collection the cheap check cannot see a permutation at all.

The topology check exists to catch exactly that. Under check_against_topology, which every production path passes, the reader walks the periodic net once per class from one atom of that class (services.topology.coordination_sequence) and matches the walk against the sequences the source supplied. A class keeps its own positional sequence whenever that sequence matches, so a correct record is left byte-identical.

What that found over the full cache: 7 records, 14 classes, whose stored positional mapping disagrees with the walk. All seven were resolved, each a clean bijection — the multiset of sequences the source stored is exactly the multiset the walk asks for, so the correction is a permutation of the source's own values and nothing is synthesised:

Net class → stored row
kgh 0→1, 1→2, 2→0
kgi 1→2, 2→1
nbo-x-d 0→1, 1→0
ssd 0→1, 1→0
tof 0→1, 1→0
ycs-z 0→1, 1→0
yct-z 0→2, 2→0

Six of those seven were invisible while the cache was a sample; only ycs-z was in it. On ycs-z, 72 of 96 atoms were being served a sequence their own connectivity contradicts.

The resolution is recorded, not silently applied. Such a row carries field_sources["cs"] = "mofplus:cs-class-mapping-resolved-by-topology" instead of a flat mofplus, plus cs_topology_mismatch naming the classes that disagreed and cs_class_mapping giving the mapping. Every checked row — 2419 of them — also carries cs_corroboration, so that "nothing disagreed" and "nothing was checked" are different states of the record rather than the same silence.

A class whose sequence could not be corroborated is withheld, not guessed. Where the walk matches no supplied sequence, or matches two that differ, that class's cs is left null, the class is named in cs_withheld, and the row carries cs_unresolved in field_sources. No record in the collection is in that state today (0 withheld), and the path is exercised by a test rather than left to be discovered.

Two limits, stated so nobody assumes otherwise. A permutation across different coordination numbers is still refused outright by the cheap invariant — the record becomes a reported failure rather than a resolved one, which is loud but is a case the resolver could handle and does not. And read_cache() still defaults check_against_topology=False; every production path passes True, but a caller reading the cache directly gets the source's positional mapping.

Separately, and not a mapping question: 19 records store no coordination sequence at all, among them raf, rag, cuz and bph. Vertex.cs is not nullable, so those vertices carry [] — absent, never a walked or invented sequence. And 13 distinct vertex-symbol strings across 12 records (dnd, dni-d, dno, dnt, gea, nzn, rht, rht-x, twf, uro, urq, urs), on 42 vertex rows in all, arrived cut off by the source at exactly 512 characters. They are stored verbatim with a note saying the string names fewer ring sizes than the vertex has angles, and must not be read as whole vertex symbols.

The loss is upstream and the stored string stays truncated — but it is no longer unrecoverable. Each of those 42 rows now has a symbol recomputed from the net's own graph, served under vertex_symbol_computed beside the fragment, never merged into it and never written into the source's column. All 42 of 42 are prefix-consistent: the computed symbol begins with exactly the text the source managed to store, up to its last complete entry. The same repair covers the 997 vertex rows whose coordination sequence the source never sent.

It does not cover lcw's six rows, where the source wrote the placeholder unk — and the reason is worth stating, because it is not a gap in the repair. lcw is a bundle of chains: each of its vertices has an angle that lies on no cycle of 40 edges or fewer, so the computation has no cycle symbol to state either. Those rows are served a ring vertex symbol (*, meaning no ring within the cap), a coordination sequence walked from the graph, and a computed_absence sentence saying in words that the ring analysis ran on the vertex and states no cycle symbol for it. A stated inability is an answer; it is not the source's placeholder repaired.

Fields no source supplies, and what became of them

None of these is an omission to be filled in later by inference. Each is a statement about what the sources contain, and each was NULL on all 3072 rows the ingest wrote; the five seeded rows keep their own values.

Four of them are no longer blank, and it is worth being exact about why. The ingest still writes nothing there, because neither source states them and nothing is inferred from a cell. They are filled by a later step — the analysis, which computes them from the net's own graph, and apply-analysis, which writes the result onto the column with computed:… provenance. A blank one is still blank for a reason, and the reason is counted rather than hidden.

Field Why it is NULL
spacegroup, spacegroup_number Neither source carries one, and that has not changed: the archive has no crystallography and a .mfpx topology file has no spacegroup line. The ingest therefore writes none, and deriving a spacegroup from the deposited cell is still forbidden — it would be a symmetry determination presented as a stored fact. What fills the column now is a different thing: the symmetry of the net's own maximum-symmetry embedding, computed from the quotient graph by Systre in the analyse step and written by apply-analysis with computed:systre-19.6.0 (<basis>) as its provenance. 2806 rows carry one; 271 are still blank, with a reason each (below).
plane_group, plane_group_number The same, for the 200 rows that are 2-periodic. Systre resolves every one of them and prints a plane group — p4mm, p6mm — which it reports on the same Ideal space group is … line it uses for a space group; the apply step reads that line behind a gate of dimension 2 instead of dimension 3 and writes it here, with the same computed:systre-19.6.0 (<basis>) provenance. 200 rows carry one and 2877 are blank, and the blank is not an absence: a 3-periodic net has no plane group. No row carries both pairs.
p, q Transitivity is carried by neither source, so the ingest writes none. p and q — the kinds of vertex and of edge — are counted from the graph by Systre and written by apply-analysis on the same 2806 rows, with the same provenance.
r, s Carried by neither source and computed by nothing here: they count the faces and tiles of a natural tiling. The tool was tried on 2026-09-12 and the limit is measured — the Gavrog library this project runs has no route from a 3-periodic net to a tiling at all, and the 200 2-periodic nets for which it has one tile the plane, whose transitivity is a triple and does not belong in an r/s pair (docs/reports/tiling-spike-2026-09-12.md). They stay blank on every row but seeded pcu, which carries 1 1 1 1 from the seed. Blank means not computed, never "none".
xyzfile This loader deposits only the canonical .mfpx; it writes no .xyz. No row in the table has one, so GET /net/{name}.xyz answers 404 on a published net and 403 on a held one.

Why the plane group is two columns and not a third meaning

spacegroup_number means an ITA space-group number, 1–230, and every consumer reads it as one: the search form bounds the field at 230, the REST parameter declares le=230, and a reader who sees 11 there takes it for P21/m. A plane group's number runs 1–17 and would land inside that range while meaning p4mm. Sharing one column would therefore mean every reader had to establish the net's dimension before it could know which numbering it was holding — and the readers that forget to ask are the defect. A discriminator column (symmetry_group_kind) leaves exactly that question open; two columns close it.

So: a 3-periodic net has spacegroup/spacegroup_number and NULL in the plane pair, a 2-periodic net has the reverse, and neither number is ever written into the other's field. The gate that decides which pair a value may reach is one predicate with one clause changed — one connected net, one copy, and a dimension of 3 or of 2 — so the two pairs cannot come to mean different things by drifting apart. p and q stay behind the 3-periodic gate for now and are blank on all 200 layer nets; whether they should follow the group across is part of the tiling question, where the 2-periodic case has to be decided anyway.

And three that are NULL on exactly the 634 RCSR-only rows, for one reason each:

Field Why it is NULL there
licence_source The archive states no terms and nobody has confirmed rcsr.anu.edu.au's in writing. A licence with no basis to name is not a licence — see above.
retrieved The archive is a file with no per-record retrieval timestamp. Nothing in it says when a block was obtained, so there is no date to record.
catenated, the cell, natoms, topofile The archive carries no crystallography and no deposit, so there is nothing to write. The cell and natoms are no longer null — see the section below — but topofile still is, and always will be: there is no deposited file, and a path in that column would claim one.

None of these appears in any row's field_sources dict, because a field with no source has no provenance to record. A NULL here means absent, which is a different claim from zero and a very different claim from inferred.

What is actually loaded

Stated here so nobody re-derives them from the reports. The MOF+ cache is complete — all 2443 nets the MOF+ listing names were retrieved, 0 failures — so the sampling cap that shaped every figure on earlier versions of this page, and the 2105 records that were "pending" under it, are history. Nothing is capped, sampled or pending.

  • The union is 3077 nets — 2296 in both sources, 634 RCSR-only, 147 MOF+-only. 3072 rows were written by the ingest; five names (pcu, pyr, qom, tbo, pcu-c) already existed as seeded demonstration rows and were enriched, not overwritten — they received the two orderings and their provenance and nothing else. Ingested rows are distinguishable from seeded ones by exactly one predicate: Net.source IS NOT NULL.
  • Every union member has a row. 0 names in the union with no row anywhere, 0 rows in the table the union does not hold.
  • "RCSR-only" is a fact about the sources now, not an artefact of a cap. All 634 are confirmed RCSR-only: MOF+ does not list the symbol at all. The pending category — a symbol MOF+ lists but the cache had not reached — is empty, and the split is still recomputed against the full MOF+ listing on every run rather than stored as a constant (reconcile.RCSR_ONLY_SPLIT_RULE), so it would report a gap again if one appeared.
  • 2443 rows carry a deposited geometry and 634 carry a computed one. Deposited geometry follows the MOF+ deposit, not the overlap: every MOF+-backed row has a cell, vertices and edges — 123316 vertex rows and 251497 edge rows in total — and every RCSR-only row had none, because the archive carries none. (2438 of those deposits were written by the run; five are the seeded rows' own, which enrichment does not touch.) The 634 RCSR-only rows now carry a geometry mofpp embed-nets computed from their own archive key, which is a different thing and is kept a different thing — the next section says how.
  • All 3077 rows are published, under the two licences set out at the top of this page: 2443 CC BY 4.0 and 634 RCSR-2019. The RCSR half was held — listed nowhere, 403 at its own URIs — until the terms were confirmed on 2026-09-09. What a reader sees today is a site listing 3077 nets, 634 of them under a licence that is not an open one.
  • The name match between the two sources was uncorroborated at ingest, and is corroborated now. The archive carries no coordination sequence — a keyword census of all 2930 blocks finds only key, version, id, checksum, ref and desc — so there is no shared source field to compare the MOF+ sequences against, and the ingest recorded all 2292 overlaps uncorroborable in field_sources.name_match: a check that could not be performed, not one that passed. The analysis supplied the missing common field by computing a canonical Systre key from the MOF+ deposit, and apply-analysis rewrote the flag: 2292 corroborated-by-systre-key, 1 conflicting-systre-key (skl-a, whose identity conflict has since been ruled on — above), 3 uncorroborable with the Systre timeout named as the reason, and 780 single-source, where there is still nothing to corroborate.

A computed geometry is not a deposit

The 634 RCSR-only rows had no cell, no vertex rows and no edge rows, so they had no picture in the viewer, no file to download, and no membership in anything that reads a vertex — including the coordination filter every search offers. What they did have is a graph, and a graph has an ideal drawing.

mofpp embed-nets runs Systre over each of those archive keys a second time, under --fullUnitCell, which prints every node and every edge of the conventional cell rather than one representative per orbit; services.embedding reads that listing back into a cell, fractional coordinates and an edge list with periodic images, and writes it onto the record. The default Systre output — the one the symmetry analysis is stored from — prints four positions for a 72-node repeat unit, which is enough to count kinds of node and not enough to draw anything, which is why this is a second run and not a re-read of the first.

It is a drawing of the net, not a measurement of a material. Every edge comes out the same length, the cell is the ideal cell of the net's own symmetry group, and the whole thing is scaled so the mean edge is 1. A real framework built on the topology has its own cell in ångström and that number is not it. So:

A MOF+ deposit A computed embedding
field_sources.vertices, .edges, cell*, natoms mofplus computed:mofpp.embedding-v1 (systre-19.6.0, rcsr-archive-key)
field_sources.geometry_note absent the sentence saying what the drawing is, served on the page and in the JSON
topofile a path in the file store NULL, always
/net/<name>.mfpx the deposited file, verbatim generated on request, with a header saying so and X-MOFpp-Geometry: computed-embedding
geometry.computed in the JSON null the same sentence
Cell shown on the record page in ångström without a unit, because the numbers are edge lengths

It is not an analysis basis either, and that is the subtler half. A net's analyses are stored per (kind, basis), and the two bases are the MOF+ deposit and the RCSR archive key. A computed embedding is a supercell of the key's own repeat unit — the same net, so the same invariants — and the apply step prefers the deposit basis, so admitting it would make the preferred account of these 634 records the one derived from the other, under a basis name asserting that MOF+ holds a file for a record MOF+ has never heard of. analysis.geometry_is_computed is the one predicate that stops it, and it reads the provenance above rather than the presence of rows.

Two things the embedding changes about what these records can do. They now draw, and they now carry a coordination_number on every vertex row — a degree read off the graph, which is a fact about the net rather than about a drawing of it — so all 634 are findable by the coordination filter for the first time. Their cs and vs columns stay empty, because those columns hold the source's coordination sequence and vertex symbol and no source states either for these records; the computed sequences and ring symbols are stored per basis by mofpp analyse-nets and served beside them.

The 200 layers, and the third axis nobody measured

200 of the 634 are 2-periodic. Their ideal cell has a, b and gamma and no third axis; their coordinates and edge images have two components each. The record's cellc, cellalpha and cellbeta are NULL and stay NULL: a layer's interlayer spacing is a property of a stacking, not of the net, and a number there would be this site's invention in a column every reader takes for a measurement. Net.cell_dimension is how a reader asks which of the three states a row is in — six parameters, three, or none.

What that costs is stated where it bites. The .mfpx format's # cell line holds six numbers, so those 200 records get no .mfpx: /net/<name>.mfpx answers 422 naming the reason rather than 404, which would deny a geometry that exists and is served in the JSON at the same URI. In the viewer a layer is drawn flat and the box around it is a parallelogram, because that is the shape of a 2-periodic cell.

Site symmetry: what kind of vertex a vertex is

vertices.symmetry was empty on all 123 315 rows and always had been. It is the column that says what kind of vertex a vertex is, and the coordination number does not: dia and nbo are both four-coordinated at every vertex, dia's sites are -43m (tetrahedral) and nbo's are 4/mmm (square-planar), and no building block fits both. That distinction is the one the reverse topological approach turns on, and until mofpp site-symmetry ran it was not in the data.

It is computed from the analyses already stored, and runs no Systre. A vertex's site symmetry is the point group of the operations of the net's ideal space group that leave the vertex where it is — its Wyckoff position in the maximum-symmetry embedding. Every net with a usable Systre analysis already carries, in that analysis, the four things that needs: the ideal space group, the relaxed cell, the relaxed position of one representative per node orbit, and the orbits themselves. The first, second and fourth were already parsed; the third was not, and is re-read from the raw_output each row stores verbatim. spglib supplies the group's operations and names the resulting point group.

Two ways of getting it wrong, both of which give plausible answers. They are worth recording because each was tried:

  1. Reading the site out of the point set's own group. Hand the expanded cell to spglib, let it determine a space group, read off its Wyckoff letters — and be wrong, because one orbit of points can have more symmetry than the net it belongs to: a net's group is constrained by its edges as well as by where its vertices are. msw is the case: Systre determines P42/nnm from the graph and spglib determines I4/mmm from the single orbit of points. The site is therefore computed as the stabiliser in Systre's group, and spglib's determination is recorded beside it as the second opinion it is — agreeing where it agrees, and named as the larger group where it is larger.
  2. Taking the first setting of the group that fits. Systre prints coordinates in some setting and does not always say which. dia's coordinates expand to 16 general positions under Fd-3m's first origin choice and to the 8 of 8a under the second; both are self-consistent and only one is tetrahedral. The setting is therefore determined: every ITA setting is tried, the orbit sizes must be a consistent whole multiple of the orbit sizes Systre itself reported, the edge count in the cell must be that same multiple of the minimal repeat unit's, and of the settings that survive the one with the fewest atoms — the most special assignment the group allows — is the answer.

What is on the collection. 2794 of 3077 records carry a site on every one of their vertex rows — 146 243 vertex rows — attributed to computed:mofpp.site_symmetry-v1 (spglib-2.7.0, <basis>), with a sentence on each record saying what the value is and which group it was computed in. The commonest sites are 1 (78 804 rows), m, 2 and mm2.

The 283 records without one are counted with a reason each, and every reason is about this project's evidence rather than about the net — a vertex always has a site in its net's own group:

Records Why no site
200 2-periodic. Their symmetry is a plane group and the Wyckoff tables used are the 230 space groups. A limit of the method, not a property of the net
52 The graph resolved into more than one connected net; there is no single group for a vertex to have a site in
21 Systre refused the graph or could not finish it, so there is no analysis to read
4 The barycentric placement puts distinct vertices on one point (Systre's own "Structure has collisions"), and two vertices on one point do not each have a site
1 No graph on any basis
8 A site was computed and could not be put on the rows: the record's vertex rows are neither the vertices of the graph it was computed on nor the conventional cell of that graph's embedding (3), or the record has no vertex rows (5)

Nothing is written unless every row of a record can be given a site. A vertex table where some rows carry one and others do not reads as a table where some vertices have none, which is a different and false claim.

spglib is an analysis-time dependency and not a runtime one. It is in the analysis extra (pip install -e '.[analysis]') beside nothing else; the site only ever reads the column this command writes, exactly as it only ever reads the values the pinned Systre jar computes. A deployment that serves the site needs neither.

Attribution

The RCSR symbols, canonical Systre keys and checksums come from the Reticular Chemistry Structure Resource:

M. O'Keeffe, M. A. Peskov, S. J. Ramsden and O. M. Yaghi, Acc. Chem. Res. 2008, 41, 1782–1789. https://rcsr.anu.edu.au

The local copy is the Systre archive RCSRnets-2019-06-01.arc. The rcsr_index derivation described on this page is ours, not RCSR's, and must not be cited as RCSR numbering. Records built from that archive alone remain unpublished; see the licence position above.