Added
-
Added ring-forming disconnections to the priority rule set, so a planning run can
propose building a heterocycle rather than only buying one. 69 of the 76 ring records
ship a hand-authoredretro_smartsnaming the reagents its reaction consumes, taking
the default set from 39 rules to 108. Seedocs/methods/priority_rules.rst,
"Ring-forming rules". -
Added
synplan.utils.frames.ChemFrame, a pandas frame that depicts any column holding
a chython object, withrules_frame,synthons_frameandtree_stats_framebuilt on
it. Seedocs/user_guide/tables.rst. -
Added a building-block check before the search in
run_search, reported as
target_in_stockin the statistics CSV and on the console. See
docs/methods/planning.rst, "Targets that are already purchasable". -
Added
ProductsTruncatedWarning, raised whenmax_productsends an enumeration
before the walk completes, so a truncated depth-first result is not mistaken for a
sample of the library. Seedocs/configuration/synthonisation.rst. -
Added
synplan.chem.reaction.curation.rebalancing, which adds the molecules an unbalanced
reaction is missing without needing atom mapping. Missing carbon is recovered
as a substructure of the reactants, and the remaining deficit is covered from
a table of small molecules. Where the reaction is mapped, the CGR names the
bonds that break, so reagents it never touched are carried through whole
instead of being cut apart. Imputed species are rejected when an atom is left
with an unsatisfied valence, which is what distinguishes Mg(OH)Br from a bare
[Mg]Br, and species that cannot survive in a flask are broken into what they
vent.
Seedocs/methods/rebalancing.rstfor the measured results and their limits.Exposed as the
rebalance_reaction_configstandardization step, with options
to name the reagent behind a redox step rather than balance it with loose
hydrogen, to refuse an answer that invents free hydrogen the record cannot
account for, to drop the products the reactants cannot have made, and to read
or ignore the atom mapping. A reagent the record names leaves as its spent
form rather than as whatever covers the arithmetic. Every answer carries a
confidence score, andmin_confidencerefuses the ones that fall below it. -
Added
scripts/rebalance_bench.py, which measures success rate and accuracy
against SynRBL's validation set. Rows whose reference is missing, or does not
itself balance, are reported apart rather than counted as losses. -
Added
TreeConfig.direction, which selects between retrosynthetic and forward search
and validates that the tree and the rollout evaluator score the same finish line —
in forward modebuilding_blocksis the goal to reach rather than the stock, and a
configuration where only one of the two carries it is now rejected instead of
silently searching toward one target and rewarding another.apply_reaction_rule
gainsco_reactants, without which a bimolecular rule run forward is handed one
structure and yields nothing. Partner selection is not implemented, so the tree does
not yet pass co-reactants during expansion. -
The test suite runs in parallel.
pytest-xdistjoins the dev group and CI runs
pytest -n auto --dist loadscope, taking CI from 224s to 79s and a local no-coverage run from
120s to 40s.loadscopekeeps each module's tests on one worker so module-scoped fixtures — the
clustering integration pipeline in particular — are built once rather than once per worker. No
test was deleted, skipped or weakened to get there; the end-to-end planner and clustering tests
that catch real regressions all still run. -
test_mcts_imports_do_not_depend_on_route_import_orderspawns its six fresh interpreters
concurrently instead of serially, 10.8s to 2.5s. Each snippet still gets its own untouched
interpreter, which is the whole point of the test; they simply no longer queue behind each
other's torch import.
Synthons (Synt-On port)
-
Added
synplan.chem.synthon, a native port of Synt-On (SynthI; Zabolotna et al., J.
Chem. Inf. Model. 2022, 62(9), 2151-2163). Seven components, none cut: building-block
classification and synthonisation, fragmentation, recombination, Bemis-Murcko
scaffolds, the rule of two, and positional analogue scanning. CLI:bb_classifying,
bb_synthonizing,synthon_fragment,synthon_enumerate,bb_scaffolds, configured
byconfigs/synthonisation.yaml. Seedocs/methods/synthons.rst. -
Added opt-in audited output bundles to the five audited synthon CLI workflows. Set
write_audit_files: trueinSynthonConfigto writefallback.smi,fallback.tsv,
errors.tsv,summary.jsonandrun.logbeside the primary output, with an exact
success/fallback input partition and atomic replacement.audit_overwritedefaults to
error; use a dedicated output directory per command. Python return values and CLI
messages are unchanged. Seedocs/configuration/synthonisation.rst. -
Added synthon coverage of a mapped reaction:
classify_coverageanswers whether a
reaction builds a bond one of the 39 acyclic disconnections already breaks, and the
synthon_coverageCLI splits a reaction file on that answer. Its use is corpus
preparation. 37.9% of a 100k mapped USPTO sample is covered. See
docs/methods/synthons.rst, "Corpus coverage". -
The shipped synthon disconnections can be used as an MCTS priority-rule set.
synthon_priority_rules()returns them under the source name"synthon"as
run_search(priority_rules=...)input. On a 40-target FDA-2020 sample at a fixed
iteration budget this solves 30/40 against a policy-only 23/40 (McNemar p=0.0156). A
synthon-stock design was built, measured at 21/40 and reverted. See
docs/methods/synthons.rst, "Using the rules for planning". -
Added heterocyclisation support to synthon enumeration: ring-forming disconnections, a
ring_pairstable andSynthonConfig.ring_closure_sizes. The reference excludes ring
closure by design; it is expressible here because a ring synthon carries product bond
orders, so no bond order is rewritten at join time. This changes default enumeration
output — across ten drug targets unique products go from 21 to 28. See
docs/methods/synthons.rst, "Enumeration". -
Curated the heterocyclisation block from 9 ring rules to 76, in two id blocks:
R16
keeps the shipped families andR17.1-R17.93holds the new ones. This changes
enumeration output and rule ids — the nineR16.1-R16.9selectors are gone, so a
rules_selectionnaming them must be rewritten. Four defects in the shipped nine are
fixed, each of which changes an answer, and ten curated rules are held out pending
chemist review. Seedocs/methods/synthons.rst, "The disconnection rules". -
Every shipped disconnection now records where it came from and what it is.
provenanceishumanfor the 78 rules converted from the reference andllmfor the
76 ring rules authored here, so the half no chemist has signed off is identifiable
rather than inferred from a rule id. Beside it each rule carriesreaction_name,
forms,reagentsandsupersedes. Seedocs/methods/synthons.rst, "Provenance and
what is not covered". -
rebalance_reaction_confignow imputes the molecules a reaction is missing
rather than round-tripping it through a CGR. The old step could only
redistribute atoms the reaction already had, and only when the mapping was
good enough to build a CGR from, so it left every genuinely unbalanced
reaction unbalanced. -
rebalance_reaction_confignow runs afterremove_reagents_configrather
than before it. Reagent removal moves spectators out of the reactants and
products, which unbalances whatever was balanced first. -
An oxidation recorded without its oxidant is now balanced with a peroxide
rather than a bare oxygen atom. chython cannot hold atomic oxygen, so an
answer spelled that way balanced in memory and came back off disk unbalanced. -
Naming the reagent behind a plain loss of hydrogen is now gated on
add_redox_agents, as its documentation always said. By default the hydrogen
is left loose, which is the honest way to say the record was written short. -
Rebalancing now withdraws the atom mapping where imputation leaves atoms on
only one side. Nothing establishes which invented atom on one side is which on
the other, and numbering them apart let a CGR compose while naming the wrong
reaction centre. A caller that needs a mapping maps the balanced reaction
itself. -
Package layout is now written down and enforced.
docs/development/package_layout.rststates
seven rules — four import layers, configuration beside its domain, verbs for pipeline stages and
nouns for things, no package nameddata, adapters beside their target named after their source,
entry points only ininterfaces, and flat packages until roughly ten modules — plus a table
saying where a new use case belongs.tests/unit/test_package_layers.pyasserts the layer graph;
three modules underutilsare recorded there as known violations so the debt stays visible and
cannot grow.Applying those rules moved several things.
synplan.synthonis nowsynplan.chem.synthon: it
imports fromchemand nothing inchemimports it back, so it was always a leaf on top of the
chemistry, not a peer of it. Inside it,enumerationbecameenumerateandreactorbecame
transformer(which also ends the collision withchem.reaction.reactor),databecamerules
because it holds the shipped rule files, andauthoringbecamerules.validatenext to the
converter that should call it. The command layer left the package:cliandauditare now
synplan.interfaces.synthon_commandsandsynplan.interfaces.synthon_audit. The priority-rule
adapter moved tosynplan.chem.reaction.rules.synthon, beside the loader it feeds.synplan.chem.databecamesynplan.chem.reaction.curation— it holds no data, only the
standardizing, filtering, mapping and rebalancing pipeline, and the old name collided with the
synthon package's genuine data directory.synplan.utils.configwas 767 lines holding every package's configuration.TreeConfigand the
evaluation configs are now insynplan.mcts.config, the policy, value and tuning configs in
synplan.ml.config,ReactorConfiginsynplan.chem.reaction.configandRuleExtractionConfig
insynplan.chem.reaction.rules.config;synplan.utils.configkeeps onlyBaseConfigModeland
NestedConfigContainer, which makesutilsa leaf again.Every old import path still works, resolved lazily through a module-level
__getattr__so that
the shims do not reintroduce the import edges the layer rule forbids, and each emits a
DeprecationWarningnaming its replacement. -
synplan.utils.parallelno longer imports torch at module level.select_deviceis its only
consumer and now imports it when called, so the process pool helpers anddefault_num_workers
are torch-free. The synthon package took the framework purely to reach
min(os.cpu_count() or 4, cap), which cost 366 ms of a 501 ms import; importing
synplan.chem.synthon.confignow takes 143 ms and pulls no torch at all. -
chython-synplanis pinned to 1.104, which adds theSynthonatom family,
SynthonContainer, the_tokenbracket field in SMILES and SMARTS,!rNas a real
excluded-ring-sizes constraint, and the depiction fixes for synthon structures.
Fixed
-
in_stockin the exported routes now means purchasable. The route exporter set the flag from
a node's position in the tree, marking every leaf available and every intermediate not, so the
file--export_routeswrites contradictedextracted_routes.jsonon the same run. -
Decomposing a RouteCGR dropped the implicit hydrogen on an aromatic nitrogen, so every azole
came back as an unmatchablec1cnc2...and the documented building-block lookup silently found
none of the routes using one. -
The protection scanner reported a pair it had never assessed as
compatible. A missing matrix
row, a missing column, or a reaction whose reacting group could not be identified now report
unknown. Scores are unchanged; only the label was wrong. -
The solvent strip list warned about its own discarded stereochemistry at import, before any
user molecule existed, which fired on--help. -
Documentation in six places said a
route_scorermakes routes come back re-ranked. Nothing
reorders:winning_nodeskeeps discovery order and neither exporter sorts, so the export is
byte-identical with and without one. Rank ontree.route_scoreyourself. -
Rebalancing no longer caps an open bond with a halide on a carbon that already
holds two oxygens. That invented carbonate halides, which balance perfectly
and do not exist. An acyl halide has one oxygen on that carbon and is
unaffected. -
Rebalancing no longer leaves a reaction whose CGR cannot be composed. Imputed
species are parsed fresh, so they started at atom 1 and landed on top of the
reaction's own numbering, and on mapped input every later standardization step
then failed. -
The fragment search no longer lets hydrogen outvote heavy atoms when reading
where a bond broke. Counting it equally picked a second acid out of an ester
rather than the alcohol that actually left. -
A halogenation written without its halogen is now balanced as elemental
halogen going in and the hydrogen halide coming out, rather than as the acid
going in and loose hydrogen venting. -
RebalanceReactionStandardizer.from_configno longer binds its options
positionally, where reordering either signature would have misbound them
silently. -
The ORD reader now chooses the desired product within each outcome rather than
across the whole record, so a second outcome that flags nothing keeps its
products and their yields. -
split_ions_configno longer fails on every ionic reaction. It read a
molecule's total charge off an attribute chython-synplan no longer has. -
Nine more tests asserted a shape rather than a behaviour, each confirmed by mutating the code they
cover and watching them stay green. The MHN ranking network had no behavioural test at all: collapsing
every rule to one identical association vector — leaving the model unable to rank anything, which is its
entire job — passed all 46 tests in the suite's largest file. It now asserts the ranking contract, that
two rules are scored apart and that permuting the rule rows permutes the logits.classify_reaction_type_detailed
was verified only byrtype in (...)tuples that included the"other"fallback, so stubbing the whole
function to return"other"passed; each case now asserts the specific label its reaction must produce.
Tree.to_stats_dictwas checked by key name, so returning every value zeroed passed. Three
RouteScannertests looped overinteractionswithout asserting the list was non-empty, so returning
[]passed; they are now one test asserting the exact interaction.test_compose_route_cgr_tree_based_invalid_route_id
never called the function it named.Precursorconstruction was asserted withis not None, so dropping
canonicalisation passed. Retro-amidation asserted only that some product came back, so emitting an amine
instead of a carboxylic acid passed. -
classify_reaction_type_detaileddocumented an'acylation'return value it cannot produce — the more
specific amide and ester branches always match first — and the changelog advertised the classifier as
12-category. Both now say 11, which is what the function actually returns. Behaviour is unchanged; the
loose tests were what let the wrong count ship. -
Four tests asserted a shape rather than a behaviour and passed against broken code. Each was
rewritten and then re-checked by re-applying the mutation that had slipped through.
test_audit_run_publishes_consistent_sidecars_and_summaryverified every artifact's provenance
hash by calling the samesha256_filethat had written it, so stubbing that function to a
constant left all 83 audit tests green; it now compares against an independent
hashlib.sha256.test_juxtaposed_recursive_primitives_are_andedcounted semicolons in the
translated SMARTS, so emitting a malformed pattern passed; it now checks that the ported query
accepts an amine and rejects both an amide and a sulfonamide, which is what the AND means.
test_no_standardizationasserted only the return type, so dropping the standardization step
entirely passed all 21 tests in the file; it now asserts that the flags change the result.
test_in_stock_flagsasserted the flag list's length, so inverting every flag passed; it now
asserts the flags.
Removed
-
Removed the
cgr_connected_components_configfilter. Everything it caught was
already caught bymulti_center_configordynamic_bonds_config, it is in
neither shipped config, and against rebalanced reactions it is actively wrong:
a dissociated leaving group is an atom with no bonds, so it is always its own
component. -
Removed the
compete_products_configfilter. It compared reagents against
products, so it only saw a competing product after atom mapping and reagent
removal had already chosen between them, and reagent removal discards the
loser outright.rebalance_reaction_config'sdrop_competing_products
replaces it, reading the imbalance instead and needing no mapping.