Getting Started¶
Prerequisites¶
- Python >= 3.10
- Rust toolchain (for building from source)
- maturin (
uv tool install maturin)
Installation¶
equiconc is a Rust library with Python bindings built via maturin. Install from source:
git clone <repo-url>
cd equiconc
python -m venv .venv
source .venv/bin/activate
maturin develop --release
Or with uv:
Quick start¶
Simple dimerization¶
Compute equilibrium concentrations for A + B ⇌ AB:
import equiconc
eq = (
equiconc.System()
.monomer("A", 100e-9) # 100 nM total
.monomer("B", 100e-9) # 100 nM total
.complex("AB", [("A", 1), ("B", 1)], dg_st=-10.0)
.equilibrium()
)
for name, conc in eq.items():
print(f"{name}: {conc:.3e} M")
Temperature¶
The default temperature is 25 °C. Set it explicitly in Celsius or Kelvin:
sys = equiconc.System() # 25 C (default)
sys = equiconc.System(temperature_C=37) # 37 C
sys = equiconc.System(temperature_K=310.15) # 310.15 K = 37 C
Energy specifications¶
You can specify complex energies in three ways:
Standard free energy (\(\Delta G^\circ\) in kcal/mol):
Dimensionless free energy (\(\Delta G / RT\), unitless). When all complexes use this form, temperature is not required:
Enthalpy and entropy (\(\Delta H\) in kcal/mol, \(\Delta S\) in kcal/(mol·K)). The free energy \(\Delta G = \Delta H - T \Delta S\) is computed at solve time, so changing temperature shifts the equilibrium:
ΔG at one temperature plus ΔS — when you have ΔG° measured at a
reference temperature and the entropy of the reaction, pass dg_st as a
(value, temperature_C) tuple together with ds_st. Equiconc derives
ΔH = ΔG + T·ΔS internally, so the resulting complex behaves like a
dh_st/ds_st pair and shifts correctly with temperature_C:
sys.complex(
"AB", [("A", 1), ("B", 1)],
dg_st=(-15.0, 37), # -15 kcal/mol at 37 C
ds_st=-0.30, # kcal/(mol K)
)
Builder pattern¶
The API uses a fluent builder pattern. Chain calls to monomer() and
complex(), then call equilibrium() to solve:
sys = equiconc.System(temperature_C=37)
sys = sys.monomer("A", 1e-6)
sys = sys.monomer("B", 1e-6)
sys = sys.complex("AB", [("A", 1), ("B", 1)], dg_st=-10.0)
eq = sys.equilibrium()
Working with results¶
The Equilibrium object supports dict-like access:
# Bracket access
conc_ab = eq["AB"]
# Membership test
assert "AB" in eq
# Iteration
for name in eq:
print(name, eq[name])
# Convert to dict
d = eq.to_dict()
# Properties
eq.monomer_names # ["A", "B"]
eq.complex_names # ["AB"]
eq.free_monomer_concentrations # [float, float]
eq.complex_concentrations # [float]
Multi-complex systems¶
Define as many complexes as needed. The solver scales with the number of monomer species (not complexes):
eq = (
equiconc.System()
.monomer("A", 1e-6)
.monomer("B", 1e-6)
.monomer("C", 1e-6)
.complex("AB", [("A", 1), ("B", 1)], dg_st=-10.0)
.complex("AC", [("A", 1), ("C", 1)], dg_st=-8.0)
.complex("ABC", [("A", 1), ("B", 1), ("C", 1)], dg_st=-15.0)
.equilibrium()
)
Tuning the solver¶
The default solver settings work for the vast majority of systems. When
you need to tune tolerances, trust-region behavior, or numerical clamps,
construct a SolverOptions and pass it to System(options=...):
import equiconc
opts = equiconc.SolverOptions(
gradient_rel_tol=1e-9, # tighter convergence
max_iterations=2000,
)
eq = (
equiconc.System(temperature_C=37, options=opts)
.monomer("A", 1e-6)
.monomer("B", 1e-6)
.complex("AB", [("A", 1), ("B", 1)], dg_st=-12.0)
.equilibrium()
)
Choosing the objective surface¶
SolverOptions exposes two trust-region paths via objective=:
"linear"(default) — minimizes the convex Dirks dual \(f(\boldsymbol\lambda)\) directly. The Hessian is positive semi-definite, so plain Cholesky always succeeds. Robust on every system the formulation can express."log"— minimizes \(g(\boldsymbol\lambda) = \ln f(\boldsymbol\lambda)\). Same minimizer, but compresses the exponential dynamic range and can converge in dramatically fewer iterations on stiff systems (very strong binding, asymmetric \(\mathbf c^0\), etc.). The price is that \(g\) is non-convex away from the optimum; equiconc compensates with modified-Cholesky regularization on indefinite Hessians and rejects any step whose model predicts an ascent.
The mass-conservation convergence test is identical for both paths, so results are interchangeable to within tolerance.
See the API Reference for the full list of
fields (max iterations, relaxed tolerances, trust-region ρ thresholds
and scales, log_c_clamp, log_q_clamp, …).
Conventions¶
- Concentrations are in molar (mol/L).
- Temperature defaults to 25 °C (298.15 K). Specify via
temperature_C(Celsius) ortemperature_K(Kelvin). - Free energies (
dg_st) are in kcal/mol with a 1 M standard state (\(u_0 = 1 \text{M}\)). - Enthalpy (
dh_st) is in kcal/mol; entropy (ds_st) is in kcal/(mol·K). - Dimensionless energies (
delta_g_over_rt) are unitless \(\Delta G / RT\) values.
Symmetry corrections
Homodimer and higher homo-oligomer symmetry corrections are not applied automatically: equiconc has no way of knowing whether the complex is actually symmetric. If your complex contains identical strands, include the symmetry correction in the \(\Delta G^\circ\) value you provide (e.g., add \(+RT \ln \sigma\) where \(\sigma\) is the symmetry number).
Error handling¶
Invalid inputs raise ValueError:
import equiconc
# No monomers
try:
equiconc.System().equilibrium()
except ValueError as e:
print(e) # "system has no monomers"
# Negative concentration
try:
equiconc.System().monomer("A", -1e-9).equilibrium()
except ValueError as e:
print(e) # "invalid concentration: -0.000000001 ..."
# Missing energy specification
try:
equiconc.System().monomer("A", 1e-9).complex("AB", [("A", 1)])
except ValueError as e:
print(e) # "must specify energy: dg_st, delta_g_over_rt, ..."