Skip to content

Tags: chython/chython

Tags

3.4

Toggle 3.4's commit message
docs: an ester's alkyl half claims it, whichever row the acyl half ma…

…tches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

3.3.2

Toggle 3.3.2's commit message
3.3.2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

3.3.1

Toggle 3.3.1's commit message
3.3.1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

3.3

Toggle 3.3's commit message
3.3: salts read as their components, stereo-tolerant reconstruction

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

3.2

Toggle 3.2's commit message
3.2: bond-level enhanced stereo, CIP descriptors, Kekule form choice

ENHANCED STEREO IS ONE NAMESPACE WITH TWO SPELLINGS.  A collection is stored at its unit's anchor slot in
`SEG_STEREO_GROUPS`, exactly as a parity is, so an axis -- a double bond, an allene, an atropisomer -- carries
a group by the same mechanism a tetrahedral centre does and there is no second plane to keep in step with the
first.  `stereo_groups()` answers `{(kind, id): [member, ...]}` where a member is an atom id for a one-owner
unit and an ascending pair for a two-owner one, so an allene reads as its two terminals and not as its chain
midpoint, which is neither owner and is only where the byte lives.  `bond_stereo_groups()` is the pair-spelled
view of the same ids, `set_stereo_group` takes either spelling and `set_bond_stereo_group` is the pair one under
its own name.  ONE ID SPACE: a bond group numbered 1 and an atom group numbered 1 are one collection, and
`canonical_bond_stereo_group_ambiguities()` names the keys the canonical colouring cannot separate, which is
what a caller reads before hashing.  A group is forgiving in and canonical out -- a pair that owns no unit is
kept as a label on its lower atom and logged, never refused.

EVERY FORMAT SPELLS THE AXIS IT CAN, AND SAYS SO WHERE IT CANNOT.  V3000 names an axis by its chain midpoint
in both directions; `mrvStereoGroup` and the CXSMILES `|&N:|` and `|oN:|` tails are atom-only and name an
owner, chosen by canonical position so the string is a function of the canonical form and not of creation
order.  pach versions 3 and 4 gain a bond-group block keyed on the owner pair, five bytes an entry and emitted
in anchor order so a decode and re-encode is byte-identical; the header field the earlier fifty records all
leave zero is now read, and the frozen v3 and v4 corpora are unchanged, which is what says the block is
additive rather than a layout change.  V2000 has one chiral bit per record and reports the collection it cannot
state.  Nothing is suppressed on a prediction: an axis with no index left in a tail is written on its lower
owner and the loss is a record.

CIP DESCRIPTORS, WHERE RULES 1a, 1b AND 2 DECIDE.  `assign_cip()` writes R and S over a hash-consed digraph
ranked sphere by sphere, ordering by atomic number, a duplicate's distance from the root, and mass.  It
RECOMPUTES: a stored descriptor is never read as a hint, a tie-break or a cache, and the computed answer
overwrites what an input stated under `cip:disagreed`.  A tetrahedral site those three rules tie, and any site
with an aromatic bond in reach, keeps no descriptor and gets one `cip:undecided` line -- the rules that settle
those sites are rule 3 and up, and `atom_cips()` answering `{}` for a configured centre is therefore an answer.
A site with no parity gets neither a descriptor nor a line, a ranking without a configuration not being one.

A DOUBLE BOND AN ALTERNATING CYCLE CAN MOVE IS A KEKULE CHOICE.  `standardize_kekule()` picks the form
`thiele()` reads, keeping the most double bonds inside a ring `thiele()` considers, and gives a ring outside the
5-7 window the alternation with the smaller canonical form.  Every form it chooses between is a valid molecule,
so its two choice records are INFO and a trial that had to move a hydrogen or a charge is a failed trial; the
512-kekulisation budget is LOST, a molecule that reaches it keeping whichever form it had.  No configuration is
stereogenic on a bond such a cycle can shift, so `validate_stereo()` drops the sign and the two shift drawings
of one compound converge whether or not either states one.  Beside it: a ring that lent its double bonds takes
them back before `thiele()`, a ring that borrows every double bond it has is counted rather than looked at, a
donor flanked by two donors is not a Kekule aromatic, an ion of carbon that states its hydrogen has no room for
a ring double, a substituted nitrogen carries the charge where it cannot carry the hydrogen, and two amidines
sharing a nitrogen are one placement ranked in one frame.

docs/stereo.rst is new -- the anchor and owners rule, the per-format encoding table, and every log row on both
the read and the write side.  docs/pach.rst gains the bond-group layout as executed samples, docs/molecule.rst
the `cip` stage and the two records a narrower arena reports through `from_bytes`, and docs/standardize.rst the
`kekule-form` stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

3.1

Toggle 3.1's commit message
3.1: absolute stereo labels, organometallic completion, persistent ra…

…dicals

ABSOLUTE STEREO SURVIVES THE ROUND TRIP.  A stored descriptor -- `{A15=R}` in the brace dialect -- is two
statements about its centre: somebody's completed determination, and that the centre is absolutely
configured, nothing determining a descriptor for a member of an OR or AND collection.  The reader takes the
second, so the centre lands in the ABS collection wherever the block named no group for that atom and a
configuration was actually stated; a `@?`, a `q` and a descriptor on a methyl are each left alone, the
collection being an inference and not the input's words.  The writer now emits `a:` whenever an atom carries
the ABS kind rather than only beside another collection, so `F[C@H](Cl)Br |a:1|` writes its label back.
Only an EXPLICIT kind reaches the field: a configured atom in no collection already means absolute and
still writes bare.  The collection is not part of the canonical form, so the labelled and unlabelled
drawings of one compound compare and hash equal -- one compound, two strings, which a cache keyed on the
text sees and one keyed on the container does not.

A ONE-COORDINATE ZINC OR GRIGNARD IS COMPLETED.  `chython/chemistry/_organometallics.py` is a `standardize()`
stage rather than a `standardize_metals.tsv` row, because no row can state it: the table writes dative
contacts and this writes a polar covalent sigma M-C bond, and the pairing has to be reproducible under input
atom order, which a rule table's first isomorphism match is not.  `CC[Zn+].[Cl-]` and all three of its
charge spellings reach `CC[Zn]Cl`; halides are ordered `Cl > Br > I > F` and metals by canonical rank, so
several reagents sharing fewer halides pair one each and every permutation of the input gives one answer.
A metal with no halide anywhere is charged instead -- `CC[Zn]` to `CC[Zn+]`, zinc and magnesium alike --
recorded under its own `organometallics:charge` id so a consumer can decline the weaker claim and keep the
join.  This is the one stage in `canonicalize()` that moves the net charge, and it says so in the log;
`canonicalize()`'s docstring scopes the invariant accordingly.

FOUR PERSISTENT-RADICAL ROWS AND ONE COUNTERION.  `functional.tsv` gains nitroxide, hindered aryloxyl,
hydrazyl and triarylmethyl radical, where the substituents are what make the radical isolable: `D3,D4` on
both alpha carbons of a nitroxide, a quaternary carbon at BOTH ortho positions of an aryloxyl, and the
unhindered member of each pair is a decoy.  The radical state is the CXSMARTS `|^1:0|` tail, there being no
in-bracket primitive, so each row covers exactly one radical atom -- which is what lets a caller compare the
named count against the record's radical count.  `salts.tsv` gains 3-hydroxy-2-naphthoic, the same formula
as `salts:xinafoic` at a different constitution, which the key mechanism keeps apart.

docs/standardize.rst gains "Screening a Record": a charge that survives the pipeline is a charge that could
not be paired off, since `neutralize()` moves a proton between components and never creates or destroys
one, so a species with nowhere to put its charge keeps it while a salt drawn apart does not.  The radical
screen is the count comparison.  What is deliberately absent -- element allow-lists, component caps,
counterion size caps -- is named as per-collection policy, `[C-]#C.[Na+]` keeping its charges so that the
discrimination stays the caller's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

3.0.2

Toggle 3.0.2's commit message
3.0.2: place leaving and arriving atoms in the transition-state view

The union was keyed by map number, so an atom present on one side only had either no key or no
partner and was left out, taking its bonds with it.  `smiles('N1CC1') @ smiles('c1ccccc1Br')` reported
the aryl carbon as 3 neighbours before and 3 after with the C-Br bond absent -- a substitution that
reads as no change at all.

The union is keyed on atoms.  An atom on one side only holds a row on the side it came from, and on
the far side counts only the neighbours that left or arrived with it: its attachment atom is
decremented by a leaving group's bonds to the retained part and incremented by an installed one's, and
a byproduct still reads as the molecule it is.  Being on one side only is not the same thing as being
unnumbered -- whether the record numbered it changes its key and nothing else, so the element,
hydrogen, degree and bond columns are identical for [CH3:1][Br:2]>>[CH4:1] and [CH3:1]Br>>[CH4:1].
`modeling_view()` keys an unnumbered atom -1 onwards in union order, `unmapped` still counting it.

The reactor, `mol_state_view` and `mol_transition_view` are untouched, so the neural mapper -- which
reads `state_view` -- is unaffected, and a molecule's transition view still emits both halves equal:
molecules and reactions keep one vocabulary.

docs/ml.rst gains "What a record can state", where a leaving atom, an arriving atom, a hydrogen count
change, a degree change and a bond order change each land, and the tests cover those five and their
combinations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

3.0.1

Toggle 3.0.1's commit message
3.0.1: keep debug info out of the Linux wheel

Linux was the only platform shipping DWARF inside its binaries, so its wheel was 12.73 MB against
6.63 MB for a two-slice universal2 macOS wheel and 4.53 MB for Windows.  Measured on 3.0's cp312
Linux wheel:

  _core.so     20.82 MB, .debug* 17.50 MB (84%), .text 2.52 MB
  libinchi.so   4.36 MB, .debug*  3.05 MB (70%), .text 1.00 MB

The code is not bigger -- Windows ships the same core as a 2.18 MB .pyd and each macOS slice is
3.43 MB.  Two separate causes:

  * distutils compiles an extension with CPython's own CFLAGS, which carry `-g`.  `-g0` appended
    after them cancels it, gcc taking the last of the pair.  `.symtab` is untouched, so a C-level
    backtrace keeps its names.
  * `CMAKE_BUILD_TYPE=Release` does not reach InChI's `-g`: its own CMakeLists adds `-g;-O1` through
    `target_compile_options`, after both CMAKE_C_FLAGS and the config's flags.  Only a link-time
    strip does, and only on Linux -- ld64 deprecates `-s` and a linked Mach-O has no DWARF anyway.

Expected: about 4.75 MB per Linux wheel.  `test_release_build.py` asserts both flags and, on any
Linux row of the matrix, reads the ELF section headers of the binaries the build actually produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

3.0

Toggle 3.0's commit message
chython 3.0: an arena-based compiled core

THE CORE.  `chython.core._core` is one Cython translation unit and the only extension.  Atoms and bonds
live in a single contiguous arena buffer, addressed by a stable id that survives edits; `map_number` is a
separate field, so a V2 `remap()` is `set_map_number()` when it meant atom-atom mapping and nothing when
it meant renumbering.  Mutation goes through an explicit edit session and derived data is recomputed on
seal.  SMILES and SMARTS read and write, isomorphism, rings, kekule/thiele, the canonical form (computed
per component), stereo, fingerprints, InChI and pach are in it.  Element, isotope and valence tables are
generated C tables compiled from `elements.tsv`, `isotopes.tsv` and `valence_rules.tsv`, and each
generator is re-run against its TSV by the suite so the two cannot drift.

WHAT IS GONE.  `algorithms/`, `containers/`, `files/`, `reactor/`, `periodictable/`, `utils/` and
`transformation/`, including the modules never ported; three ratchets fail if one of those names becomes
importable again.  There is no metaclass-generated element class and no `CGRContainer` --
`ReactionModelingView` is the CGR-free view chytorch consumes.  chython 2.24 remains reachable as an
out-of-process differential oracle, which is the only sanctioned way to consult V2.

THE LAYERS.  `chemistry` holds chemical knowledge as TSV beside the passes applying it; `reactions` is
its sibling, holding SMIRKS templates, the functional and protective corpora and mapping reconstruction;
then `formats`, `depict`, `interop`.  Nothing imports the façade and the dependency direction never
reverses, both ratcheted.  Registration onto the sealed container is by injection, not inheritance.

INPUT IS GARBAGE BY DEFAULT.  A reader never rejects a record for being chemically wrong: an illegal
valence, a nonsense charge and an underivable hydrogen count are stored and logged, the last as a
reserved `H_UNKNOWN` rather than a silent zero.  Repair is an explicit pipeline afterwards -- `kekule()`,
`standardize()`, `fix_resonance()`, `thiele()` -- and refusals live at the answer boundary alone.

Documentation is `docs/`, where every Python sample is executed and every figure re-rendered and compared
against the committed file.  About 8700 tests; CI builds five platform rows and a sixth that adds the
differential oracles and reports coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

2.24

Toggle 2.24's commit message
reconstruct: try salt components alongside the intact reactant record

A logged reactant routinely bundles a spectator counterion with the reactive
species (K/Na trifluoroborate, amine.HCl, sodium alkoxide) in one container.
Handed the whole thing, the reactor faithfully carries the spectator into the
generated product, which can then never equal the salt-free target, so a real
match is lost.

Components are now offered as extra candidates *in addition to* the intact
container, never instead of it: the counterion is sometimes retained on both
sides (amine.HCl -> amine'.HCl) and that only reconstructs from the intact
form. The intact form is listed first and every phase returns on first
success, so records that already matched keep their exact result.
_mixes_views bars a subset from pairing a record with its own component, while
still allowing two components of one record (a co-formulated pair).

ELN corpus (17222 records): 785 -> 968 matched, no losses, no label changes,
no new errors; all 183 gained mappings validate as CGRs. Runtime unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>