Skip to content

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:

uv sync
uv run maturin develop --release

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):

sys.complex("AB", [("A", 1), ("B", 1)], dg_st=-10.0)

Dimensionless free energy (\(\Delta G / RT\), unitless). When all complexes use this form, temperature is not required:

sys.complex("AB", [("A", 1), ("B", 1)], delta_g_over_rt=-16.2)

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:

sys.complex("AB", [("A", 1), ("B", 1)], dh_st=-50.0, ds_st=-0.13)

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()
)

Conventions

  • Concentrations are in molar (mol/L).
  • Temperature defaults to 25 °C (298.15 K). Specify via temperature_C (Celsius) or temperature_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, ..."