Changelog¶
Changelog¶
All notable changes to htpolynet will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
[2.1.0] - 2026-06-01¶
Added¶
New
htpolynet.repairpackage implementing a postcure topology-repair stage that sits between cure and postcure. Drivers can do bond-breaking, atom deletion, atom transfer between residues, and re-templating — operations the monotonic cure/cap reaction machinery cannot perform.repair/__init__.pydispatches eachpostcure_repairconfig entry by itstype:field;repair/topology_surgery.pyprovides the generic edit primitives (delete_bondswith cascading angle/dihedral/14-pair cleanup,set_atom_attributes,reassign_residue,add_bonds_with_templatewrappingmake_bonds+map_from_templates+ an int-dtype rescue for atom-index columns that pandas float-promotes via NaN-tainted concat);repair/cyanate_cap.pycarries the first concrete driver. A newreaction_stage.repairenum value lets repair-stage reactions ride the existing symmetry-expansion and parameterization paths so the cure-template lookup at surgery time uses a properly parameterized linked-product Molecule. The runtime gainscfg.postcure_repair,Dirs.systems_repair, and ado_repair()hook wired intodo_workflowbetween cure and postcure, including a steepest-descent + short NVT relaxation pass to absorb LJ clashes from relocated cap atoms.New
triazine_to_cyanate_caprepair type: the BADCy-specific driver inrepair/cyanate_cap.py. At finite cure conversion, the topological A2+B3 BADCy model in example 6 leaves artifacts that don’t exist in a real undercured thermoset — free BPA-OH groups and bare triazine C-H sites instead of -O-C#N end-groups. Atom-conservation (the count of unreacted bridge-OH atoms equals the count of dangling crosslinker C atoms across the whole system, exactly) lets the driver dismantle every incomplete triazine (k < full_bond_countbonded BPAs) into three independent -C#N fragments via a within-ring C-N matching; thekfragments already bonded to a BPA become BPA-O-C#N caps in place, and the remaining3 - kare transferred to the nearest unreacted BPA-OH withincap_search_radius(greedy matching with radius expansion + global-nearest fallback). After the surgery the heavy-atom neighbors of each deleted sacrificial H absorb its lost charge viaadjust_charges, keeping the system net-neutral for Ewald. Topology-level outcome on the small test: 19 incomplete TAZ → 57 CYN residues + 1 surviving TAZ + exact heavy-atom conservation, with the C-N bond resolving to GAFF c1-n1 (0.115 nm sp triple) and the BPA-O-C bond to os-c1 (0.132 nm aryl-cyanate ether).Example 6 (
6-cyanate-ester.yaml) rebuilt around the topological A2+B3 + postcure-repair architecture. The BPA-O-C#N cyanate-ester core is now represented topologically: BPA (90 → 360 at 4x scale, two reactive phenolic O atoms) reacts with bare 1,3,5-triazine TAZ (60 → 240, three reactive ring C-H atoms; ring N atoms additionally atom-mapped to N1/N2/N3 so the repair driver can refer to them by name) in a simple cure-stage aryl-ether substitution — no in-cure ring closure, nobondcycle_collectivebypass needed because the triazine ring is pre-formed in the TAZ monomer rather than constructed via 3-way cyclotrimerization during cure. A new auxiliary CYN building block ([CH:1]#[N:2], hydrogen cyanide; not inserted into the box, exists only as a parameterization template) plus arepair-stagecap_with_cyanatereaction supplies the auto-generatedBPA~O1-C1~CYNlinked-product template the repair driver splices into the system for every cap. Apostcure_repair: [{type: triazine_to_cyanate_cap, ...}]block at the end of the YAML drives the conversion. The header comment block explains the topological model, its tradeoff vs. the previous cyclotrimerization model (no cure-kinetics realism, faithful final-network structure), and how the repair stage restores BADCy residual chemistry.New
htpolynet/profiling.pymodule: aRunProfilewith a stage-stack context manager (profiling.stage('name')) and a subprocess-attribution path. Every external command routed throughexternal/command.run(and the two rawsubprocess.runsites inexternal/smiles_input) is timed and classified —gmx-mdrun,gmx-grompp,antechamber,parmchk2,tleap,obabel,rdkit, etc.do_workflowwraps each stage (setup,initialization,densification,precure,curewith one nested frame periter-K,capping,postcure,final) so subprocess time is attributed to whichever stage was active when the call happened. At end-of-run a formatted table is written to the log (one line perlogger.infocall, nomy_loggerasterisk padding) and a machine-readableproj-N/profile.jsonis dumped besidefinal.top.New
CURE.controls.min_bonds_per_iterationknob (default10). The bond-search loop now grows the radius until at least this many bonds have been found, falling through to whatever count exists atmax_search_radius. The effective floor is clamped againstbond_target(remaining bonds needed to reachdesired_conversion) andbond_limit(themax_conversion_per_iterationcap), so demanding e.g.min_bonds_per_iteration: 50near end-of-cure never stalls the build. The post-loop “ifnbonds > 0proceed, elsesearch_failed” branch is preserved — accepting fewer bonds than the floor (when max radius is reached) still triggers relax + equilibrate as before. Empirically on the DGEBA/PACM example,min_bonds_per_iteration=10cuts the cure iteration count from 41 (with=1) to 15; raising further to=20saves only one more iteration. The default of 10 was picked off that diminishing-returns curve.The “Radius increased to N nm” log line now also reports the cumulative bond count and the iteration’s min-bonds floor as
(X/Y eligible bonds so far). Makes it visible at a glance whether the floor orbond_targetis the active constraint as the search radius grows.scripts/run_all_examples.sh— runs every depot example sequentially in its own subdirectory under./examples-runs/. Discovers the example ID list by parsinghtpolynet fetch-example --help(with a0..4fallback). Idempotent: skipsfetch-exampleif a YAML is already in the per-example directory. Reports per-example exit status and exits non-zero if any example failed. Pass-through after--is forwarded tohtpolynet run.scripts/run_all_examples.sh --force-reparameterize— convenience flag that forwards--force-parameterization --force-checkinto everyhtpolynet runcall. Each example re-runs antechamber/parmchk/tleap on its monomers and overwrites the user cache (~/.htpolynet/molecules/parameterized/). Appropriate rigor when consecutive examples share monomers but differ in reaction sets — sidesteps the cache-poisoning interaction between e.g. example 0 (no reactions) and example 1 (cure reactions on STY).All five bundled examples (0–4) are now self-contained YAMLs using the RDKit atom-mapping path on each constituent, so the user names reactive atoms by chemical identity (e.g.
[CH2:1][CH3:2]) instead of by obabel’s output ordering. The legacy.shand.tgzsiblings have been removed;htpolynet fetch-example Nnow delivers a single.yamlfor any N. Example 4 (DFDA/FDE) additionally gets areactive_atomsentry forO1/O2that was missing in its prior shell script (the cap reaction references them).htpolynet fetch-example 1delivers the YAML directly; usage collapses tohtpolynet run 1-polystyrene.yaml. The legacy1-polystyrene.shand1-polystyrene.tgzhave been removed.fetch-examplenow prefers.yaml>.sh>.tgz.Final-stage save now emits
final.viz.psf(real bond topology, written via parmed fromfinal.top+final.gro) andfinal.viz.tcl(drops any bond longer than 3 Å from the display) alongside the existingfinal.gro/final.top/final.tpx/final.grx. Load withvmd final.viz.psf final.gro -e final.viz.tclto view a crosslinked network without the “long bonds across PBC” artifact. The TCL usestopo getbondlist both/topo setbondlist both $list— the valid topotools 1.x flag values aretype,order,both,none; the earlierallreturned an empty list silently and reported “PSF appears to carry no bonds”. The TCL also prints the first bond’s measured length so the user can verify VMD loaded coordinates in Å.New CLI subcommand
htpolynet make-vizregeneratesfinal.viz.psf+final.viz.tclfrom anyfinal.top+final.gropair without re-running the full workflow. Defaults assume the current directory hasfinal.topandfinal.gro(i.e. you’vecd’d intosystems/final-results/); override with-top/-gro/-prefix.VMD viz now ships a sidecar
<prefix>.viz.macros.tclof constituent-keyedatomselectmacros, sourced automatically from<prefix>.viz.tcl. Two layers:<NAME>selects every atom of every instance of constituent<NAME>(e.g.GMApicks all 75 bis-GMAs in example 2;DHTpicks all 50 linear HTPB chains in example 5), and<NAME>_<NNN>picks one specific instance by global molecule index. The macros are built fromfinal.grx’smolecule/molecule_namecolumns and compress contiguous atom ranges into VMDindex A to Btokens (so e.g. example 5’s 11 KB macros file covers 225 instances). Lets a user highlight chemical entities like bis-GMA or HTPB whose internal residue scheme reflects building blocks (BPA+2×HIE,OB+TB×n+TBO×2) rather than the assembled molecule. The residue-level view is untouched —resname TBOetc. still work — the new macros are additive.htpolynet make-vizgains a-grxflag (auto-detected from the-grostem) so the macros are also generated when invoked standalone.Two follow-ons to the
find_templatebystander relaxation, both needed so the small-fragment cure idiom works end-to-end:Molecule.idx_mapperspreviously asserted that the template and instance had the same bystander count on each side, and built atom-pair mappings by flat-concatenating the per-side bystander lists into one zip. With subset-bystander matching the lists can legitimately differ in length, and the flat concatenation misaligns side-A and side-B bystanders across the zip. Pair each region (bonded residues, side-A bystanders, side-B bystanders, oneaways) in its own zip so a length mismatch in one region doesn’t shift the alignment of others, and drop the exact-count assertion.map_from_templatescopies the template’s angle / dihedral / pair tables into the system after mapping template atom indices throughtemp2inst. For cyanate-ester cure, the template’s CY has more atoms than the post-build system’s CY (the cure-reactive C consumes one H during build, so a system CY has one fewer H on that side than the fresh-from-SMILES cure-template CY does); the template’s H atom that doesn’t exist in the system maps to NaN. Filter rows whose mapped atom indices contain any NaN before concatenating into the system topology — those rows are force-field parameters for atoms that don’t exist in the cured system. No effect on chemistries where all template atoms have system counterparts (examples 1-5).
find_templatenow uses subset semantics on bystanders. A parameterization-stage templateTmatches a system-instance bondBif every (bystander_resname, bystander_atomname) pair declared byTalso appears inB;Bis allowed to carry additional bystanders thatTdoesn’t mention. Oneaway context, atom names, residue names, and theintraresidueflag still require exact equality. When multiple templates match, the one with the most bystanders declared wins, so chain-extension templates produced bybondchain_expand_reactions(which carry specific bystanders) still beat the bare dimer template when their additional context is exactly the in-chain instance’s. This unblocks the “small-fragment cure reactant” idiom in cases like cyanate-ester cure, whereCY.C1is intramolecularly bonded toBPA.O1in every BCY-embedded instance — the bare CY+CY dimer template carries no BPA bystander, but the BPA bystander is structural context, not bond chemistry, and the subset rule lets the small template match anyway. Verified: example 1’s iter-2 chain-context bond still picks the trimer chain-extension template over the bare dimer template via strict-oneaway discrimination; example 6’s iter-1 CY-CY-in-BCY bond now matches the CY+CY cure template via subset-bystander relaxation.Fixed: tleap-input ordering in
external/ambertools.pyrancheck mymolbeforeloadamberparams <frcmod>, so any GAFF-coverage gap that parmchk2 had already patched (e.g.h5-ce-n2on cyanate-ester C=N–C=N dimer templates) still showed up in tleap’s output as an earlyError!. The run-wrapper’s override needle then fired and aborted the parameterization, even though tleap actually completed and the.top/.crdfiles were valid. Reordered to load the frcmod before the check so the patched parameters are in scope when the molecule is validated. Unblocks new depot example 6 (cyanate-ester thermoset) whose C=N–C=N open-chain cure dimer falls in a GAFF coverage gap that parmchk2 patches by analogy. Other examples are unaffected — for chemistry where parmchk2 emits no patches, the reorder is a no-op.Fixed: cached monomer
.grxfiles in~/.htpolynet/molecules/parameterized/carry reactivity-related attributes (z,sea_idx,bondchain,bondchain_idx) that are YAML-dependent — they reflect the reactions defined for the run that wrote the cache, not anything intrinsic to the monomer. Running example 0 (liquid styrene, no reactions) wroteSTY.grxwith all-zero z; running example 1 (polystyrene) afterward then loaded that cache and produced 0 candidate atoms in the cure bond search, silently stalling at “Radius increased to N nm (0/10 eligible bonds so far)” all the way out to the max radius. Fix: in the cache-hit branch of_generate_molecule, for monomers (no generator), re-runinitialize_monomer_grx_attributes()against the current run’szrecsso z/sea_idx/bondchain are derived from this YAML rather than inherited from a stale cache. The cache itself can still be written with run-specific z values; only the load-time interpretation is hardened.Fixed: corollary of the monomer-cache-poisoning fix. When STY was first poisoned with z=0 by example 0, the subsequent example-1 run generated and cached the cure-stage dimer (
STY~C1-C2~STY.grx) and the cap (STYCC.grx) with emptybondchaindata — the dimer’schain_manager.injest_bondno-ops when neither atom is in a chain, which is exactly what happens when the upstream monomer’s chain_manager was empty at the time. On the next example-1 run, the cached dimer loaded with 0 chains,bondchain_expand_reactionsfound no 4-atom chains to extend, and zero chain-context templates were generated — so CURE iteration 1 worked (only the dimer template was needed) but iteration 2 raised “you have a bond for which I cannot find a template” because the C1-C2 bond now had aoneawaySTY chain partner that no available template captured. Fix: after loading a build product from cache, compare the total chain-atom count carried by the cachedchain_manageragainst the sum across the product’s reactants’chain_managers; if the cache carries fewer atoms (either zero chains, or a partial chain — e.g. example 2’s hetero-dimerSTY~C1-C2~HIEcame out length-3 instead of length-4), treat as stale, reset the molecule’sTopoCoord/chain_manager/bond_templates/reaction_bonds/sequenceto a blank state, re-parameterize via the normalgenerate()path, and overwrite the cache. The state reset is needed because the cache-load steps populateTopoCoord(whichgenerate()will then re-merge reactants into) and the half-loaded state ends up float-promoting theglobalIdxcolumn on the merged dataframe, crashing the prebonding-mol2 writer. Verified on examples 1 and 2: regenerates the affected dimers, after whichbondchain_expand_reactionsproduces the expected chain-extension templates (3 for example 1, 32 for example 2 — up from 12 before).New depot example
6-cyanate-ester.yaml: bisphenol-A dicyanate ester (BADCy) thermoset. The BCY constituent is assembled at param-stage from a BPA bisphenol-A core plus two single-carbonCYcyanate end-groups (formaldimine,[CH2:1]=[NH:2]— drawn in the sp2 imino-formate active form so the cure-stage triazine-forming C-N bonds have one sacrificial H pre-allocated on each side). Mirrors example 2’sGMA = BPA + 2 HIEbuild pattern. The cure stage forms C-N bonds between cyanate end-groups on different BCYs via a singlecyclizereaction; three such bonds among three monomers close into the 1,3,5-triazine ring (the characteristic crosslink of a cured cyanate ester). Thebondcycle_collectivering-suppression check is C-C-specific via theChainManager, so the heteroatom triazine ring is allowed to close unhindered. Available ashtpolynet fetch-example 6. Note: pair with--force-parameterization --force-checkinwhen extending the YAML to cover atoms not previously named in any reaction (e.g. CY’s N1) — cached build products inheritzrecs-derivedzvalues from the prior YAML’s reactions and won’t pick up newly-added reactivity otherwise.write_topnow casts known-int columns (atom indices, function codes, dihedral periodicities,nrexcl, etc.) to pandas’ nullableInt64before serialization, so they emit as e.g.2rather than2.0. The float form had been silently accepted bygmx gromppbut rejected byparmed’s gromacs top reader, which broke the new.viz.psfgeneration.gmx --versionoutput is now parsed forGPU support:(CUDA, OpenCL, SYCL, disabled) and shown alongside the version line in the startup banner.Consistency check: if the YAML config sets
mdrun_options.gpu_idbut the installed gmx was built without GPU support, or no GPU devices are visible on the host, the option is dropped and a warning is logged. This prevents the runtime crash thatgmx mdrun -gpu_id 0produces when zero devices are detected.Cache hits during parameterization are now logged at INFO (“Using cached parameterization for
<name>”) instead of DEBUG, plus a post-loop summary line tallying reused vs freshly-parameterized molecules and a reminder of the--force-parameterization --force-checkinflags to invalidate stale entries. Pairs with the new “Parameterization caching” section in the user-guide.The
-restartflag now emits a prominent runtime warning that resumption is experimental and known to fail at the first cure-stage topology update; the argparse help string is annotated likewise, anddocs/source/user-guide/usage.rstcarries an expanded warning explaining the root cause (in-memory cure state is not fully reconstructible fromcure_state.yaml+ on-disk topology files). Pre-cure stages still resume correctly; this section is parked pending a redesign of cure-state persistence.Fixed:
htpolynet run -restartfailed inCureState.from_yamlwith aConstructorErrorfor the!!python/object:tag, because curecontroller’s loader wasyaml.FullLoader(which recent PyYAML tightened to reject Python object tags) while the matchingyaml.dump(self)writes those tags. Switched toyaml.Loaderto match whatcheckpoint.pyalready uses.Fixed: rebuild
self.chain_managerfrom the reloaded coordinates on restart.do_initializationis correctly skipped by the checkpoint decorator on resume, but it’s also wherechain_managerwas being constructed, so subsequent stages (do_cure) hitAttributeError.do_workflownow reconstructs it from the loaded TopoCoord whenever a checkpoint payload is present.Fixed:
htpolynet run -restartcould die withshutil.SameFileErrorwhen the userlibrary search fell through toprojPathand the cwd already lived inside it (so the source file IS the destination).projectfilesystem.pynow uses a_safe_copyfilehelper that no-ops when src and dst resolve to the same path.Fixed: example 2 HIE constituent’s SMILES used
[C:1](zero implicit H by SMILES bracket-atom rules) instead of[CH:1], so the α-carbon was emitted at valence 3, antechamber typed itc2, and tleap failed with “no angle parameter for o - c2 - os”. Documented the bracket-atom H-count gotcha indocs/source/user-guide/molecular-structure-inputs.rst.Fixed: the RDKit SMILES path now goes through an SDF (molfile) intermediate to obabel rather than PDB. PDB does not carry bond orders, so obabel had to re-infer them and frequently mis-assigned a carbonyl carbon as the alkene sp2 type (
C.2from aC-Osingle bond rather thanC.2+O.2double-bond pair), which propagated to GAFF asc2instead ofcand broke tleap with “no angle parameter for o - c2 - os” on monomers with ester groups (e.g. HIE in example 2). SDF preserves bond orders, so obabel emits the right sybyl types and antechamber assigns the correct GAFF types.Fixed: SMILES-generated mol2 files were being written to
projPath/lib/molecules/inputs/<NAME>.mol2becauseRuntime.__init__runs afterpfs._setup_project_dirhaschdir’d into the project directory.pfs.checkout()looks in the user library (rootPath/lib/...), so the files were unfindable and molecule generation fell through to aBPA.pdb/STY.pdbassertion.materialize_smiles_inputsis now invoked with an absoluteinputs_dirrooted at the user library, andhtpolynet runpre-createslib/molecules/{inputs,parameterized}/at startup so the library is wired up even when the user’s working directory had nolib/.Constituents in the YAML config may now carry a
smiles:key. When present, htpolynet generateslib/molecules/inputs/<NAME>.mol2itself before parameterization, eliminating the obabel/sed boilerplate that example shell scripts have historically duplicated. Reactive atom names are set via eitherrename_atoms: {<1-based-index>: <name>}(obabel path, always available) orreactive_atoms: {<smiles-map-num>: <name>}(RDKit path, used when the SMILES contains[*:N]atom-mapping tokens and RDKit is importable). RDKit is an optional extra:pip install 'htpolynet[smiles]'; the container ships it by default.New
docker-entrypoint.shthat auto-detects the host owner of the/workbind mount and drops privileges viagosubefore invokinghtpolynet. Users no longer need to set--user,HOST_UID/HOST_GID, or any other env vars — output files are written with host ownership automatically. The script also writes an/etc/passwdentry for the runtime uid sogosuresolvesHOMEto/home/htpolynetrather than falling back to/.Entrypoint dispatches by inspecting the first argument: if it resolves to an executable on
PATH(bash,python,obabel, …) it is exec’d directly; otherwise it is treated as anhtpolynetsubcommand. This makes it possible to rundocker compose run --rm htpolynet bash 1-polystyrene.sh --run(i.e. drive the example shell scripts that themselves callobabel/htpolynet).
Changed¶
Docker-related files moved from the repo root into a new
docker/subdirectory:docker/Dockerfile,docker/compose.yml,docker/docker-entrypoint.sh. The GH Actions build workflow and the docscurl -OURL are updated to match.Dockerfile installs
gosuand uses the new entrypoint script.compose.ymlsimplified: nouser:field; noHOST_UID/HOST_GIDsubstitution. The entrypoint handles uid mapping at runtime.compose.ymlbind mount uses${PWD}:/work:Zso SELinux-enforcing hosts (Fedora, RHEL, openSUSE Tumbleweed, …) relabel the host directory tocontainer_file_t; without this the container is denied writes regardless of POSIX permissions. Harmless on non-SELinux systems.compose.ymlsetsMPLCONFIGDIR=/tmp/matplotlibin the environment to silence matplotlib’s “not a writable directory” warning.
Fixed¶
_do_pappreviously applied the_nonempty_directivesguard only to the anneal branch; preequil and postequil ran whenever the section was truthy. That broke for any YAML that definedprecure: preequilibration: ...without an explicitprecure: postequilibration—_apply_runtime_defaultsinjects the runtime-default postequilibration block (ps: 0, i.e. “no postequilibration”),TC.equilibratereturnsNoneforps: 0, and the trailingtrace('Density', edr_list, ...)crashes with'NoneType' object is not iterable. Same guard now wraps all three branches, mirroring the existing anneal pattern, with a comment explaining why the postcure-default ps=0 default behaves as it does.Example 6 (postcure anneal) first cycle segment changed from
ps: 20tops: 0, matching example 3’s pattern. The earlier value pushedannealing_time[0]to 20 ps whileinit_twas 0, trippinggmx grompp’s “First time point for annealing > init_t” fatal error at the postcure stage; the leading ps=0 segment exists purely to anchor the annealing protocol at simulation time 0.Topology.rep_exwas shifting the per-copyresnrbyc(the copy index) instead ofc * residues_per_copy, so when a multi-residue molecule (e.g. the assembled HTPB chains DHT/THT in example 5, ~41 residues each) was replicatedNtimes, the resid ranges of successive copies overlapped almost completely. Single-residue monomers like IPD happened to work becausec * 1 == c, which is why no prior example hit this. The collision surfaced downstream inMolecule.idx_mappers(CURE iter-1 topology update): the resid-keyed filter pulled atoms from several distinct instances at once, the atomName merge fanned out, and the same template atom got mapped to multiple instance atoms, tripping the “temp_idx N already claimed” sanity check. Fix: captureresnr_per_copy = max(resnr)before the concat and use it as the shift unit. Existing init.gro/init.top files with corrupted resids must be regenerated (deleteproj-*and re-rundo_initialization).TopoCoord.bondcycle_collectivecrashed withAttributeError: 'NoneType' object has no attribute 'added_bonds'when a cure bond’s endpoints weren’t part of any vinyl C-C bondchain (e.g. the urethane O-C linkages between TBO oxygen and IPDI formyl carbon in example 5). The chain-manager’sinjest_bondis a no-op when both atoms are outside the chain graph (andcreate_if_missing=False), so the subsequentchain_of(r.ai)returnsNone. Such bonds can’t form a C-C bondcycle by chemistry — guard the loop and skip them.Molecule.generate_conformersnow reuses existing conformer.grofiles instead of regenerating them on every invocation. The check is on file presence in the cwd (molecules/parameterized/), so it kicks in equivalently on-restartand on re-runs in an existing proj dir. Changingcountupward still triggers regeneration because the expected files won’t all be present. For example 5 (HTPB chains use 6 gromacs-generated conformers per stereoisomer per long-chain monomer), this skips a substantial amount of work on every restart.Dockerfile: pre-create
/home/htpolynetwith mode0777so named docker volumes mounted there inherit a world-writable initial state. Without this, a freshhtpolynet-homevolume came up owned by root and the non-root container user could not create~/.htpolynet,~/.config/matplotlib, etc.ParmEd
GromacsWarning: The [ pairs ] section contains N exceptions that aren't 1-4 pairs; make sure you know what you're doing!at end of run. CURE can shorten the topological distance between two atoms previously templated as a 1-4 pair down to 1-3 (a new cure bond shortcuts the original 3-bond path), but nothing was pruning those now-invalid pair entries. NewTopology.prune_stale_14_pairs()walks the bondlist and drops any[ pairs ]entry whose endpoints aren’t actually 1-4 in the post-cure graph (uses the existingbondlist.partners_of, so it’s O(degree³) per pair — fast). Wired intoRuntime.save_datajust beforewrite_top; an INFO log line reports the count when any entries get pruned. On the DFA/FDE example 4 build (~23k pairs total) it dropped 40 stale 1-3 pairs.Silent no-op config keys in the depot example YAMLs:
initial_search_radius(schema issearch_radius) andlate_threshhold(schema islate_threshold, no extrah) were being silently swallowed bydict.get(key, default)because the values happened to match defaults. Renamed in all four CURE-bearing examples (1, 2, 3, 4). Behavior unchanged but the docs can now quote the keys honestly.Silent no-op
nconformers:keys on FDE and DFA in example 4. The runtime reads conformer settings from aconformers:sub-block (withcount,generator,minimizekeys); the flatnconformers:line was never read. Dropped.Default
CURE.controls.min_bonds_per_iterationis now10(was effectively1, gated bywhile nbonds == 0). This is silent for users with custom YAMLs that don’t pin the key; rerun behavior shifts toward fewer, larger CURE iterations.
Documentation¶
Container-usage page rewritten around the entrypoint-driven uid mapping; covers SELinux +
:Zand the role of thehtpolynet-homenamed volume.Full rewrite of every short-build tutorial against the current self-contained-YAML workflow. Tutorial directory names now match the depot stem:
2-DGEBA-PACM/→3-pacm-dgeba-epoxy-thermoset/,3-VE-STY/→2-bisgma-styrene-thermoset/, and a brand-new tutorial 4 (4-dfda-fde-epoxy-thermoset/) was written. All cross-references (ve_*→bgs_*,dgeba_*→pde_*, newdfe_*) renamed accordingly. Each tutorial walks the YAML block-by-block vialiteralinclude, drops the obsoleterun.sh/obabel/sedmonomer-prep machinery, and points readers at the newprofile.jsonandmin_bonds_per_iterationknob where relevant. Tutorial 0’s results page now ships thefinal-box.pngVMD render.scripts/run_all_examples.shdocumented in-script via its header block.Removed the legacy
src/htpolynet/resources/cfg/directory (12 orphaned config snippets with no Python references; pre-example-depot artifacts). Updated the one:download:reference inusage.rstthat pointed to a file in there.
[2.0.1] - 2026-05-12¶
Changed¶
compose.yml: bind-mount source switched from.to${PWD}so a single sharedcompose.ymlreferenced viadocker compose -fmounts the caller’s working directory rather than the file’s directory.compose.yml:user:field now reads${HOST_UID}/${HOST_GID}instead of${UID}/${GID}— bash’sUIDis read-only andGIDis not exported, so the original form silently fell back to0(root) and broke writes into the bind mount under rootless Docker.compose.yml: container now gets a persistentHOMEvia a namedhtpolynet-homedocker volume — without this,~/.htpolynetresolved to/.htpolynet(root of the container fs) for the non-root user and the user cache could not be created. The named volume also keeps parameterized monomers/oligomers around acrossdocker compose run --rminvocations.
Fixed¶
Dockerfile header comments: removed duplicated
htpolynettoken in the exampledocker runinvocations (theENTRYPOINTalready provides it) and added--user $(id -u):$(id -g)so output files are not owned by root.
Documentation¶
Container-usage page now notes that the image is published only to GHCR (a bare
docker run htpolynetresolves against Docker Hub and fails) and shows adocker tagshortcut for a local alias.Added a
curl -Oone-liner for fetchingcompose.ymldirectly from the repo.
[2.0.0] - 2026-05-07¶
Changed¶
Package renamed from
HTPolyNettohtpolynet(fully lowercase) for PEP 8 compliance and PyPI consistency.Runtime now logs the HTPolyNet git commit hash at startup, with a warning when uncommitted changes are present.
Added¶
Apptainer/Singularity container support: distributed as a
.sifimage for reproducible execution on HPC clusters.New
gen-slurm-scriptsubcommand generates a ready-to-submit SLURM batch script from an htpolynet YAML config file.
Fixed¶
Chain-expansion bug: bond-chain
ChainManagerwas not rebuilt for monomers on the fetch path, causingbondchain_expand_reactionsto produce no chain-extended oligomers in runs that reused cached parameterizations.
[1.0.9] - 2025-01-01¶
Added¶
minimum_bondcycle_lengthparameter to allow for cyclic polymerization above a certain threshold length.
Fixed¶
Rings not transferred from monomer templates if they are pre-parameterized.
Atom indexes in bondchain structure not remapped after atom deletion.
[1.0.8] - 2024-01-04¶
Changed¶
Uses
chordless_cyclesto find rings;ringidxis no longer a unique atom attribute; improved ring-pierce detection.