Changelog¶
[Unreleased]¶
Changed¶
- Releases (PyPI, crates.io, plus a Forgejo release with all wheels
attached) are now produced from Codeberg via
.forgejo/workflows/builds.yml. The GitHub workflow at.github/workflows/builds.ymlis reduced to a CI mirror that runs Linux lint + Python smoke tests on push/PR. The Codeberg Generic Packages registry is intentionally not used as a release mirror, and CI artifact retention is kept short (1 day for wheels/sdist, 3 days for the docs/web bundles), to stay under Codeberg's 1.5 GB per-repo storage quota. - PyPI publication switched from
--trusted-publishing always(no Codeberg/Forgejo OIDC support yet) to aPYPI_TOKENAPI token.
Removed¶
- macOS
x86_64wheels are no longer produced. Apple Silicon (aarch64) wheels are still published for every supported Python. Intel macOS users should install from the published sdist or pin the last release that shipped Intel wheels (equiconc==0.4.1).
[0.4.1] - 2026-04-25¶
Fixed¶
- The 0.4.0 PyPI publish was blocked by a stale
version = "0.3.0"inpyproject.toml: maturin built every wheel as 0.3.0, which collided with the existing 0.3.0 record on PyPI.pyproject.tomlnow usesdynamic = ["version"]and lets maturin source the version fromCargo.toml, so a single bump there propagates to both the Rust crate and the Python wheel. The 0.4.0 Rust crate (already on crates.io) is unaffected; this is purely a re-publish to land the Python wheels of the same release.
[0.4.0] - 2026-04-25¶
Added¶
-
System::solve_with_progress(on_iter)/IterationStatus/SolveControl/EquilibriumError::Aborted. The new method invokes a callback once per outer trust-region iteration (linear and log paths both); the callback may returnSolveControl::Abortto short-circuit withEquilibriumError::Aborted. The existingSystem::solve()is now a thin shim that supplies a no-op callback, so its behavior and performance are unchanged. Intended as the hook for live progress reporting from the web UI's worker, but useful anywhere a long sweep wants to surface convergence telemetry. -
Browser-side equilibrium-concentration solver as a separate
web/crate (Leptos + Trunk). Builds to a single static-site bundle (trunk build --release→web/dist/); no backend, no JS code, all compute runs in WebAssembly via the unmodifiedequiconcsolver. The page exposes the fullSolverOptionssurface tiered Basic / Advanced / Expert, includesequiconc-defaultsandCOFFEE-compatiblepreset buttons, a kcal/mol vs RT energy-units toggle (with the temperature input hiding itself when not consumed), drag-and-drop file loading for.cfe/.ocx/.con, baked-in testcases, sortable concentrations table with per-monomer share-of-mass and 100-row pagination for COFFEE-scale 50k-species systems, a horizontal bar chart of top species by absolute concentration, a per-monomer share-of-mass stacked bar chart, a compact live convergence chart (log₁₀ ‖∇‖ vs. iteration), and TSV / CSV / JSON-report exports. The solve runs in a dedicated Web Worker so the UI thread stays responsive; a Cancel button terminates the worker mid-iteration. Newjust webandjust web-devrecipes; CI builds the dist artifact and onmain/ tag pushes deploys it to GitHub Pages alongside the docs (docs at the project URL root, web app at/app/). pub mod equiconc::iowithparse_cfe(text, n_mon)andparse_concentrations(text)parsers for NUPACK-style complex tables (.cfe/.ocx, with NUPACK header auto-detection) and one-value-per-line concentration files (.con). Accepts whitespace,,,;, and|as delimiters.pub fn equiconc::water_molar_density(t_c)— molar density of liquid water from the Tanaka 2001 mass-density formula. Useful for converting between molarity and mole fraction in callers that mirror COFFEE's "scalarity" wrapper.equiconc::Equilibrium::mass_balance_residual/mass_balance_residual_selfand a free-functionequiconc::mass_balance_residualformax_i |c0_i − Σ_j A_{ji} c_j|.
Changed¶
equiconc-coffee-clinow consumesequiconc::io::parse_cfe,equiconc::io::parse_concentrations,equiconc::water_molar_density, andequiconc::mass_balance_residualinstead of carrying its own copies. No behavior change.-
The repository is now a Cargo workspace; the published
equiconcpackage is unchanged but the workspace also contains theequiconc-webcrate (publish = false). -
New
simdCargo feature (opt-in, off by default) that vectorizes the per-species element-wise hot loops inevaluate_intoandevaluate_log_intoviapulpruntime ISA dispatch (SSE2 / AVX2 / AVX-512 on x86, NEON on aarch64, scalar on wasm). Enable withcargo build --features simd(or--features python,simdfor the Python wheel). Translates to ~9% end-to-end speedup on the COFFEE-largeequiconc_lineartestcases; preserves scalar performance onequiconc_log.
Numerical contract: linear-path kernels use a degree-12 Taylor
polynomial after Cody-Waite range reduction (≤2 ulps vs libm);
log-path kernels keep parallelism for everything around the exp
call but route the exp itself through scalar libm per lane, because
the trust-region step acceptance on g = ln f requires
per-iteration progress measurable above 4·eps·|g| and the
polynomial residual stalls the iteration on extremely stiff systems
(COFFEE testcase 0 with sub-nanomolar c0 and ~54k species was the
canonical failure). The full and candidate evaluators always agree
on rounding so the trust-region ρ check stays consistent.
[0.3.0] - 2026-04-25¶
Added¶
-
Optional log-objective trust-region path, selected via
SolverOptions::objective = SolverObjective::Log(Rust) orSolverOptions(objective="log")(Python). The default remainsSolverObjective::Linear, so existing callers see no behavior change. The log path minimizesg(λ) = ln f(λ)rather than the linear dualf(λ); on stiff systems (very strong binding, asymmetricc⁰) it often converges in many fewer iterations becausegcompresses the exponential dynamic range off. The log objective is non-convex (H_gcan be indefinite away from the optimum); equiconc compensates with on-the-fly modified-Cholesky regularization of the model Hessian, so the dog-leg step always sees a PD matrix andpredicted_reduction > 0by construction. The implementation structurally avoids the three documented failure modes of COFFEE's log-Lagrangian solver (seecoffee-bugs.mdandcoffee/docs/issue2-analysis.md): -
Bug 1 (NaN from
∞ − ∞):evaluate_log_intocomputes the objective via log-sum-exp on the un-clampedlog_q + Aᵀλ, never formingfand then taking its log. Steps that would pushf ≤ 0are rejected and the trust region shrinks. - Bug 2 (premature convergence at
λ ≈ 0under strong binding): the convergence test is on the primal mass-conservation residual|Ac − c⁰|_i < atol + rtol · c0_ifor both objectives — never on the log-rescaled gradient∇g = ∇f / f, which is the term COFFEE suppresses to floating-point underflow. - Bug 3 / coffee issue #2 (trust-region oscillation on indefinite
Hessians): the model Hessian is regularized to PD before dog-leg
sees it, and a defensive
pred_reduction ≤ 0 → ρ = -1sentinel catches any residual case where regularization saturates.
Validated against COFFEE on the existing
tests/proptest_vs_coffee.rs cross-check (new
prop_equiconc_log_matches_coffee) and against the linear path on
tests/proptest_equiconc.rs (prop_log_matches_linear). Explicit
reproducers for the three documented coffee failure cases now live in
src/lib.rs (coffee_bug1_positive_dg_conformer_log,
coffee_bug2_strong_binding_log, coffee_issue2_strong_dimer_log).
- New optional binary equiconc-coffee-cli (gated behind the coffee-cli
Cargo feature) that accepts the same NUPACK-style .ocx/.cfe +
.con input files as COFFEE's coffee_cli and produces the same
space-separated 2-decimal-scientific results payload. Hard-codes
T = 37 °C, mole-fraction scaling, and the ΔG ≥ -230 kcal/mol
clamp to match COFFEE's non-configurable defaults — producing
byte-for-byte agreement with coffee_cli on the monomer free
concentrations of ../coffee/testcases/{0,1,2} at the {:.2e}
output precision, and on the full 8-species payload of testcase 2.
Integration tests in tests/coffee_cli_compat.rs verify per-species
agreement on a synthetic 2-monomer/1-dimer system and on all three
COFFEE testcases (skipped if ../coffee/testcases/ is absent).
Build with cargo build --release --features coffee-cli.
- cargo-deny configuration (deny.toml) and a CI job that runs three
checks against the runtime dependency tree:
- licenses: fails on any SPDX expression outside the allow-list
(MIT, Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause,
Unicode-3.0).
- advisories: fails on any RustSec vulnerability, unmaintained
crate, unsound advisory, or yanked version.
- sources: fails if any runtime crate comes from anywhere other
than the default crates.io registry.
Dev-dependencies (criterion, proptest, and the cgevans/coffee git
dep used for cross-checks) are excluded via [graph] exclude-dev
since they ship in neither the crate tarball nor the wheel.
- CI lint job: cargo fmt --all --check,
cargo clippy --all-features --all-targets -- -D warnings, and
cargo doc --no-deps --all-features with RUSTDOCFLAGS=-D warnings.
Catches formatting drift, clippy regressions, and broken intra-doc
links ahead of a docs.rs publish.
- CI cross-platform / cross-Python smoke tests (tests-matrix job):
the existing tests job runs only on Linux + Python 3.12 because of
the cargo-llvm-cov coverage instrumentation; the new matrix exercises
the corners that ship in the PyPI wheel but were never otherwise
tested — minimum supported Python (3.10), free-threaded Python
(3.13t), macOS, and Windows.
- Dependabot configuration (.github/dependabot.yml) for weekly Cargo,
GitHub Actions, and uv (Python) dependency updates, with non-major
bumps grouped per ecosystem to reduce PR churn.
Changed¶
cargo fmt --allsweep acrosssrc/,tests/,benches/, andexamples/. No behavior change; lands ahead of the newcargo fmt --checkCI gate so the gate starts green.- Clippy cleanup so
cargo clippy --all-features --all-targets -- -D warningspasses. Mechanical fixes: deriveDefaultonSolverObjective; collapsefield_reassign_with_defaultpatterns in tests into struct literals; rewrite!(f > 0.0) || !f.is_finite()asf <= 0.0 || !f.is_finite()(NaN-equivalent, clippy-clean);iter().copied().collect()→to_vec(); minor doc-comment re-indentation in benches;for i in 0..nindex loop →enumerate; collapse nestedif letinto a Rust 2024 let-chain. Type aliasesComplexSpec/PyComplexSpecintroduced for theVec<(String, Vec<(String, usize)>, _)>builder fields.#[allow]annotations with one-line comments where the lint can't tell the code is intentional: NaN-safe!(a < b)rho ordering check inSolverOptions::validate;too_many_argumentson theevaluate_into/evaluate_log_intohot-path inner functions and on the pyo3-boundPySystem::complexmethod. builds.ymlnow usesconcurrency: cancel-in-progresskeyed on${{ github.ref }}forpull_requestevents, so superseded PR runs are cancelled. Tag and main pushes are unaffected.- Reverted the crate
licensefield from"BSD-3-Clause AND Apache-2.0"back to"BSD-3-Clause". The dual declaration existed only because of the vendored COFFEE sources; with vendoring removed, the published crate contains no Apache-2.0-licensed code. - Comparative benchmarks, the
proptest_vs_coffeecross-validation test, and theinstrument_large/instrument_xldiagnostic examples now depend on thecoffeecrate as a pinned git dev-dependency (via thecgevans/coffeefork, which gates the polars-backed file-input path behind a feature) withdefault-features = false, instead of carrying a vendored copy undertests/coffee_vendor/. These are all dev-only consumers, so the git source doesn't affect the published crate. Because COFFEE still pinsndarray = 0.16while equiconc is on 0.17, an aliasedndarray_coffee = { package = "ndarray", version = "0.16" }dev-dep supplies the array types COFFEE's API requires. instrument_large/instrument_xlno longer report a COFFEE iteration count (upstream'sOptimizerdoesn't expose one); wall time only.- Bumped
criteriondev-dependency from 0.5 to 0.8.
[0.2.0] - 2026-04-18¶
Changed¶
- BREAKING: Renamed the Rust
Systembuilder type toSystemBuilder. The newSystemis a stateful solver handle that owns numerical inputs, work buffers, and the most recent solution; it supports in-place mutation for titration / parameter sweeps and re-solves with warm-started λ. The one-shot pattern is nowSystemBuilder::new()…build()?.solve()?instead ofSystem::new()…equilibrium()?. - BREAKING:
Equilibriumis now a borrowed view (Equilibrium<'a>) into the owningSystemrather than an owned struct. Useeq.get(name),eq.at(idx), or indexing (eq["AB"],eq[idx]) for lookups. To keep results past aSystemmutation, copy the data out (e.g.eq.concentrations().to_owned()). The borrow checker now enforces "no stale reads": mutating accessors onSystemcannot fire while anEquilibriumview is alive. - Duplicate monomers in a complex composition now have their counts summed instead of raising an error.
- BREAKING (Rust): bumped
ndarrayfrom 0.16 to 0.17. Downstream crates consuming equiconc'sArrayView{1,2}/Array{1,2}-valued API must upgrade ndarray in lockstep.
Added¶
SolverOptionsstruct exposing previously-hard-coded solver knobs:max_iterations, gradient tolerances (full + relaxed), trust-region parameters (initial / max δ, ρ thresholds, shrink / grow scale factors), stagnation threshold, and two numerical clamps (log_c_clamp, optionallog_q_clamp). Every field has a default matching the previous constant, soSolverOptions::default()reproduces pre-configuration behavior bit-for-bit.SystemBuilder::options/options_ref,System::options/options_mut/set_options, plusSystem::from_arrays_with_optionsandSystem::from_arrays_with_names_and_optionsfor passing options directly alongside numerical inputs.EquilibriumError::InvalidOptionsvariant, raised bySolverOptions::validate()on inconsistent combinations (non-positive tolerances,shrink_rho >= grow_rho, etc.) and surfaced by every constructor that accepts options.- Python
equiconc.SolverOptionsclass with keyword-only constructor mirroring the Rust fields. Pass toSystem(options=opts). System::from_arraysandSystem::from_arrays_with_namesfor constructing a solver directly from numerical inputs without going through the string-keyed builder. Temperature is not stored at this level — callers bake it intolog_q.System::c0_mut,System::log_q_mut,System::set_c0,System::set_log_qfor in-place mutation in titration / parameter sweep workflows.System::last_solutionreturningOption<Equilibrium<'_>>,Noneif any input has been modified since the last successful solve.System::validateto re-run structural invariant checks after caller-driven mutation.EquilibriumError::InvalidInputsvariant, raised byfrom_arrays/from_arrays_with_nameswhen shapes, the identity block, monomerlog_q, or name tables are inconsistent.
Python bindings are unchanged.
[0.1.0] - 2026-03-10¶
- Initial release