Observed since 20 Oct, 19:00 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
ADAlib: Anti-Derivative Approximator (ADA) library for ODE forward simulation, inverse estimation, operator learning, and MPC, with the ADAlib Studio browser GUI
adalib-IFL)ADAlib is a Python library built on the Anti-Derivative Approximator (ADA) representation for ordinary differential equations. One library covers four tasks: forward simulation, physics-informed inverse parameter estimation, amortized operator learning, and model predictive control (MPC) with the trained operator as a differentiable, batchable surrogate.
From PyPI:
pip install adalib-IFL
or the latest version from GitHub:
pip install git+https://github.com/yooyoungwoo7/ADAF_Library.git
or from a local clone:
git clone https://github.com/yooyoungwoo7/ADAF_Library.git
cd ADAF_Library
pip install -e .
Requires Python >= 3.10 and TensorFlow >= 2.13. Tested with Python 3.13.5 and
TensorFlow 2.20.0; requirements.txt pins the exact tested environment
(pip install -r requirements.txt).
import adalib # the PyPI distribution is "adalib-IFL"; the import name is "adalib"
Note: an unrelated PyPI package called
adalibuses the same import name. Do not install both in the same environment.
pip install adalib-IFL also installs ADAlib Studio, a local browser
workspace for all four tasks: type the governing equations, choose the
settings, run, and inspect or export the results, without writing code.
adalib-studio # or: python -m adalib_studio
The server starts and the app opens at http://127.0.0.1:8000 (the next free port if 8000 is taken). Stop it with Ctrl+C.
| Option | Meaning |
|---|---|
--port 8100 | serve on a specific port |
--runs DIR | where job results go (default ~/adalib-studio/runs) |
--max-parallel 2 | jobs computed at the same time (default 1) |
--no-browser | do not open the browser |
--host 0.0.0.0 | let other computers on the network connect (default: this computer only) |
Every module has an AI assistant panel (right column, with your own Anthropic or OpenAI API key, set in the panel's Settings). A sentence such as "damped pendulum with gamma = 0.5 for 10 s", or a pasted picture of the equations, becomes the module's setup: the system and solver settings (Forward), the controlled system and training size (Operator), the parameters to estimate and the synthetic data (Inverse), or the plant, targets and controllers (MPC). It can also restyle any figure with full Plotly access ("remove the minor ticks", "legend under the plot", "8 x 6 cm at 600 dpi"). Answers are checked like a run request and fill the panels; you review them and press Run or Train. The run history can be ordered by time or grouped by module, and each run can be deleted.
Everything runs on your machine. The browser only shows the interface; a local
FastAPI server accepts each job and starts a separate Python worker process
that imports adalib and computes it. Results, logs and trained surrogates
stay under the runs directory across restarts.
Troubleshooting on Windows:
DLL load failed ... blocked by application control policy: Windows 11
Smart App Control is blocking TensorFlow's DLLs (any TensorFlow install, not
only ADAlib). Turn it off under Windows Security > App & browser control >
Smart App Control settings.pip install fails with a long-path error: TensorFlow's files exceed the
260-character path limit in deep folders. Create the virtual environment in
a short path such as C:\Users\<you>\adalib-studio, or enable long paths.The adalib_studio/ directory is generated from the
ADAlib Studio repository;
change the GUI there, not in this repository.
| Feature | User-defined CallableODESystem | Built-in systems |
|---|---|---|
| Forward | ✅ Fully supported | ✅ Supported |
| Operator learning | ✅ Physics-residual only (LPA Operator NN) | ✅ Supported |
| MPC — tracking | ✅ LPA Operator NN surrogate | ✅ Supported |
| MPC — economic | ❌ Not yet supported | ✅ Supported |
| Inverse (parameter estimation) | ⚠️ Works; validated on LV/Euler only | ⚠️ Validated on LV/Euler only |
Built-in systems: cstr, triple_tank, fedbatch_bioreactor,
lotka_volterra, lotka_volterra_ur, euler, damped_pendulum
(see Built-in systems).
Inverse caveat: the inverse solver is validated only for
lotka_volterraandeulerunder low noise and full observation. Inverse estimation forfedbatch_bioreactoris known to fail (the Haldane kinetics are poorly identifiable), and forcstr/triple_tankit is unverified.
import adalib
def rhs(t, x, u=None, p=None):
return [-x[0]] # dy/dt = -y
def rhs_tf(var_list, i, u=None, p=None):
y, y_t = var_list[0]
return y_t - (-y) # ADA-F physics residual
system = adalib.CallableODESystem("decay", rhs, rhs_tf=rhs_tf, state_names=["y"])
options = adalib.ForwardOptions(basis="adaf", n_seg=10, epochs=5, use_lbfgs=True)
result = adalib.run_forward(system=system, x0=[1.0], t_span=(0.0, 3.0), options=options)
t = result.solution.t # (Nt_total,)
y = result.solution.y # (n_state, Nt_total)
import adalib
system = adalib.get_system("cstr")
options = adalib.OperatorOptions(
basis="lpa", n_train=2000, epochs=1000,
work_dir="./runs/cstr_operator",
)
result = adalib.run_operator(
system=system,
x0=[0.8, 0.5, 134.14, 130.0],
t_span=(0.0, 0.5),
options=options,
)
t, y = result.t, result.y # rollout at segment boundaries
print(result.paths["work_dir"]) # all artifacts saved here
import adalib
system = adalib.get_system("cstr")
options = adalib.MPCOptions(
mode="tracking", basis="lpa",
target={"T_R": 136.0}, n_steps=20,
n_train=200, epochs=300,
work_dir="./runs/cstr_mpc",
)
result = adalib.run_mpc(
system=system,
x0=[0.8, 0.5, 141.0, 141.0],
options=options,
)
t, x, u = result.t, result.x, result.u # closed-loop plant trajectory
import adalib
def msd_rhs(t, state, u=None, p=None):
x, v = state
F = u[0] if u else 0.0
m, c, k = 1.0, 0.3, 1.5
return [v, (F - c * v - k * x) / m]
system = adalib.CallableODESystem(
name="mass_spring_damper",
rhs=msd_rhs,
state_names=["x", "v"],
control_names=["F"],
state_bounds={"x": (-3.0, 3.0), "v": (-4.0, 4.0)},
control_bounds={"F": (-5.0, 5.0)},
)
options = adalib.MPCOptions(
mode="tracking",
controlled_variables=["x"],
target={"x": 1.0},
dt=0.4, horizon=5,
tracking_weights=[10.0], control_weights=[0.05],
n_train=400, n_val=80, generate_data=True, train_operator=True,
epochs=100, n_steps=25,
work_dir="./runs/msd_mpc",
)
result = adalib.run_mpc(system=system, x0=[0.0, 0.0], options=options)
t, x, u = result.t, result.x, result.u
cstr, triple_tank)The operator surrogate is a pure TF graph (MLP → panel weights W → linear LPA basis), so the horizon-H rollout cost is exactly differentiable w.r.t. the control sequence, and B candidate sequences can be evaluated in one batched forward pass.
import adalib
# Exact dJ/du via automatic differentiation through the operator (SLSQP jac)
result = adalib.run_mpc(
system="triple_tank", x0=[190.0, 100.0, 140.0],
options=adalib.MPCOptions(
target={"h3": 150.0}, horizon=5, n_steps=20,
gradient="autodiff", # "fd" = same cost, finite differences
work_dir="./runs/tt_autodiff_mpc",
),
)
print(result.metadata["opt_ms_per_step_mean"], result.metadata["opt_nfev_mean"])
# Sampling MPC exploiting batched surrogate inference (CEM or MPPI)
result = adalib.run_mpc(
system="triple_tank", x0=[190.0, 100.0, 140.0],
options=adalib.MPCOptions(
target={"h3": 150.0}, horizon=5, n_steps=20,
optimizer="CEM", cem_samples=512, cem_iters=8, # or optimizer="MPPI"
work_dir="./runs/tt_cem_mpc",
),
)
Supported for cstr and triple_tank (tracking) and fedbatch_bioreactor
(economic — same gradient / optimizer options with mode="economic").
The generic CallableODESystem MPC path also accepts gradient="autodiff",
using an analytic Jacobian through the pure-numpy LPA surrogate.
gradient=None + optimizer="SLSQP" (default) keeps the original loops.
See examples/mpc/surrogate_mpc_showcase.py for a head-to-head comparison
(FD vs autodiff vs CEM, plus a batch-throughput microbenchmark against
solve_ivp).
import adalib
import numpy as np
import tensorflow as tf
# --- Define system with unknown parameters ---------------------------------
def lv_rhs(t, x, u=None, p=None):
prey, pred = x
alpha, beta, gamma, delta = p["alpha"], p["beta"], p["gamma"], p["delta"]
return [
alpha * prey - beta * prey * pred,
delta * prey * pred - gamma * pred,
]
def lv_rhs_tf(var_list, i, u=None, p=None):
(prey, prey_t), (pred, pred_t) = var_list
alpha = p["alpha"]; beta = p["beta"]
gamma = p["gamma"]; delta = p["delta"]
r1 = prey_t - (alpha * prey - beta * prey * pred)
r2 = pred_t - (delta * prey * pred - gamma * pred)
return tf.stack([r1, r2], axis=-1)
system = adalib.CallableODESystem(
"lotka_volterra", lv_rhs, rhs_tf=lv_rhs_tf,
state_names=["prey", "predator"],
)
# --- Generate synthetic observations ---------------------------------------
# (For a fair benchmark, sample observations from an independent solver such as
# scipy.integrate.solve_ivp rather than from ADAlib's own forward solution;
# see examples/inverse/lotka_volterra_inverse.py.)
true_p = {"alpha": 1.0, "beta": 0.1, "gamma": 1.5, "delta": 0.075}
ref = adalib.run_forward(system, x0=[10.0, 5.0], t_span=(0.0, 15.0),
params=true_p,
options=adalib.ForwardOptions(n_seg=30))
data = adalib.data_gen(ref, n_points=60, noise_std=0.05, seed=42)
# --- Set up inverse problem ------------------------------------------------
params = {
"alpha": adalib.InverseParameter(0.5, lower=0.0, name="alpha"),
"beta": adalib.InverseParameter(0.2, lower=0.0, name="beta"),
"gamma": 1.5, # known — plain float
"delta": 0.075, # known — plain float
}
options = adalib.InverseOptions(
n_seg=30, epochs=50, adam_lr=1e-3, adam_inner=100,
lambda_data=10.0, lambda_physics=1.0,
training_strategy="joint",
normalize_data_loss=True,
)
result = adalib.run_inverse(system, x0=[10.0, 5.0], t_span=(0.0, 15.0),
params=params, data=data, options=options)
print(result.estimated_params) # {"alpha": ..., "beta": ...}
result.plot(save_path="inverse_result.png")
result.plot_loss(save_path="inverse_loss.png")
result.plot_params(true_params=true_p, save_path="inverse_params.png")
ADALib ships publication-quality plot helpers in adalib.utils.
import matplotlib
matplotlib.use("Agg") # headless / CI — call before importing adalib
import adalib
adalib.utils.set_adalib_plot_style() # "sans" (default) or "serif"
fig, axes = adalib.utils.plot_forward_result(
result,
reference=lambda t: np.exp(-t), # callable, scipy OdeResult, or (t, y) tuple
state_names=["$y$"],
title="exponential decay",
save_path="forward_result.png",
)
fig, axes, metrics = adalib.utils.plot_operator_result(
[r1, r2, r3],
reference=[ref1, ref2, ref3], # list of (t, y) tuples or scipy OdeResults
state_names=["$C_A$", "$C_B$", "$T_R$", "$T_K$"],
state_groups=[[0, 1], [2, 3]], # optional grouped layout
labels=["Case 1", "Case 2", "Case 3"],
save_path="operator_result.png",
)
print(metrics["l2_rel"]) # shape: (n_cases, n_state)
fig, axes = adalib.utils.plot_mpc_result(
all_results, # MPCResult or list of MPCResult
state_names=["$C_A$", "$C_B$", "$T_R$", "$T_K$"],
control_names=["$\\dot{Q}$"],
target={"T_R": 136.0}, # dashed setpoint line
labels=[f"IC {i+1}" for i in range(5)],
save_path="mpc_result.png",
)
Every workflow returns a result object with built-in convenience methods.
result = adalib.run_forward(system, x0=[1.0], t_span=(0.0, 3.0))
t = result.t # result.solution.t also works
y = result.y # result.solution.y also works
result.plot(reference=lambda t: np.exp(-t), save_path="forward.png")
result.plot(reference="solve_ivp", save_path="forward_ref.png")
t, y = result.to_arrays()
result.save_npz("forward.npz")
print(result.list_artifacts())
result = adalib.run_operator(system, x0=..., options=...)
result.plot(reference="solve_ivp", save_path="operator_rollout.png")
result.inference_plot(n_cases=4, save_path="operator_inference.png")
cases = result.infer(n_cases=4) # list of {"t", "y_op", "y_ref", "u", "x0"}
result.save_inference({"t_0": cases[0]["t"], "y_0": cases[0]["y_op"]})
print(result.list_artifacts())
result = adalib.run_mpc(system, x0=..., options=...)
result.plot(save_path="mpc.png")
result.operator_inference_plot(n_cases=4, reference="solve_ivp",
save_path="mpc_surrogate.png")
t, x, u, cost = result.to_arrays()
result.save_npz("mpc.npz")
print(result.list_artifacts())
result = adalib.run_inverse(system, x0=..., t_span=...,
params=params, data=data, options=options)
print(result.estimated_params) # {"alpha": 0.998, "beta": 0.102, ...}
print(result.runtime_sec) # wall-clock time
# Trajectory
t, y = result.t, result.y # (Nt_total,) and (n_state, Nt_total)
# Plots
result.plot(save_path="trajectory.png") # recovered vs observed
result.plot_loss(save_path="loss.png") # total / physics / data loss curves
result.plot_params(true_params={"alpha": 1.0}, # parameter convergence
save_path="params.png")
# History
print(result.loss_history) # list of total loss per step
print(result.param_history) # dict of list, one entry per log step
t, y = result.to_arrays()
result.save_npz("inverse.npz")
result.save_all("output/") # trajectory + loss + params plots + npz
| Field | Default | Description |
|---|---|---|
basis | "adaf" | Basis type ("adaf" only for inverse) |
n_seg | 20 | Number of piecewise segments |
N_p | 5 | ADA-F basis order per segment |
N_m | 100 | ADA-F collocation points |
Nt_total | 1000 | Total time-grid points for output |
gamma | 0.8 | ADA-F decay parameter |
epochs | 5 | Outer passes over all segments |
adam_inner | 200 | Adam steps per segment per epoch |
adam_lr | 1e-3 | Adam learning rate |
use_lbfgs | True | L-BFGS polish after Adam |
lambda_physics | 1.0 | Physics residual loss weight |
lambda_data | 10.0 | Data fit loss weight |
training_strategy | "joint" | "joint" (W+θ together) or "alternating" (W then θ) |
data_prefit_steps | 0 | Adam steps fitting W only before joint training |
normalize_data_loss | True | Scale data loss by number of observations |
normalize_physics_loss | False | Scale physics loss by collocation count |
warm_seg_passes | 1 | Extra passes on early segments |
n_warm_segs | 3 | Number of early segments to warm-repeat |
true_params | None | Ground-truth dict for convergence plots |
output_dir | None | Auto-save plots/npz here if set |
Training strategy guidance:
"joint" (default): works well when lambda_data / lambda_physics ≤ 10."alternating": recommended when the ratio exceeds ~100 (e.g. lambda_data=500), to prevent data loss from dominating physics gradients.# Unconstrained
p = adalib.InverseParameter(initial=0.5, name="alpha")
# Lower bound only (softplus transform)
p = adalib.InverseParameter(initial=0.5, lower=0.0, name="alpha")
# Box-constrained (sigmoid transform)
p = adalib.InverseParameter(initial=0.5, lower=0.0, upper=2.0, name="alpha")
# Read current estimate during/after training
print(p.numpy_value) # Python float, post-transform
print(p.constrained) # TF tensor used inside ODE
Runnable scripts are in examples/:
examples/simple_api/)| Script | Description |
|---|---|
01_forward_example.py | Forward — user-defined exponential decay |
02_forward_euler_example.py | Forward — built-in Euler rigid body |
03_operator_example.py | Operator — CSTR (3 ICs, scipy reference) |
04_mpc_example.py | MPC — CSTR tracking (5 ICs) |
05_generic_tracking_mpc.py | MPC — user-defined mass-spring-damper |
examples/forward/)| Script | Description |
|---|---|
lotka_volterra_forward.py | Lotka-Volterra forward simulation |
euler_forward.py | Euler rigid body forward simulation |
examples/operator/)| Script | Description |
|---|---|
train_bioreactor_operator.py | Operator learning — fed-batch bioreactor |
examples/mpc/)| Script | Description |
|---|---|
cstr_tracking_mpc.py | Tracking MPC — CSTR |
triple_tank_tracking_mpc.py | Tracking MPC — triple tank |
bioreactor_economic_mpc.py | Economic MPC — fed-batch bioreactor |
surrogate_mpc_showcase.py | FD vs autodiff vs CEM comparison + batch-inference throughput benchmark |
examples/inverse/)| Script | Description |
|---|---|
lotka_volterra_inverse.py | Parameter estimation — Lotka-Volterra (α, β) |
euler_inverse.py | Parameter estimation — Euler body inertia |
Inverse legend: ✅ validated · ⚠️ unverified · ❌ known to fail.
| Name | States | Operator | MPC | Inverse |
|---|---|---|---|---|
cstr | C_A, C_B, T_R, T_K | ✅ | ✅ tracking | ⚠️ |
triple_tank | h1, h2, h3 (cm; time in s) | ✅ | ✅ tracking | ⚠️ |
fedbatch_bioreactor | Xs, Ss, Ps, Vs | ✅ | ✅ economic | ❌ |
lotka_volterra | U, R (scaled prey, predator) | ✅ | — | ✅ |
lotka_volterra_ur | r (prey), p (predator) | — | — | — |
euler | ω₁, ω₂, ω₃ | — | — | ✅ |
damped_pendulum | θ, ω | — | — | — |
print(adalib.list_systems())
# ['cstr', 'damped_pendulum', 'euler', 'fedbatch_bioreactor',
# 'lotka_volterra', 'lotka_volterra_ur', 'triple_tank']
tests/ holds runnable scripts, one per feature. Most of them train a model
when executed (some for many minutes), so run them individually rather than
with a bare pytest tests/:
python tests/test_adalib_forward_euler.py # one script
pytest -q tests/test_adalib_mpc_autodiff.py # the fast pytest suite
| File | Coverage |
|---|---|
test_adalib_forward.py | Forward — user-defined system |
test_adalib_forward_euler.py | Forward — Euler rigid body |
test_adalib_forward_lotka.py | Forward — Lotka–Volterra |
test_adalib_forward_pendulum.py | Forward — damped pendulum |
test_adalib_forward_ev_itms.py | Forward — additional user-defined system |
test_adalib_inverse_lv.py | Inverse — Lotka–Volterra |
test_adalib_inverse_euler.py | Inverse — Euler rigid body |
test_adalib_inverse_pendulum.py | Inverse — damped pendulum |
test_adalib_operator.py | Operator — CSTR (paper-size settings) |
test_adalib_operator_triple_tank.py | Operator — triple tank (paper-size settings) |
test_adalib_operator_speaker.py | Operator — user-defined system |
test_adalib_operator_ev_itms.py | Operator — additional user-defined system |
test_adalib_mpc.py | MPC workflow |
test_adalib_mpc_forward.py | MPC with forward reference |
test_adalib_mpc_bioreactor.py | Economic MPC — fed-batch bioreactor |
test_adalib_mpc_autodiff.py | Autodiff / CEM surrogate MPC, gradient-vs-FD consistency (pytest) |
ADAF_Library/
├── adalib/ # the installable package
├── adalib_studio/ # ADAlib Studio GUI (generated copy; `adalib-studio` command)
│ ├── _vendor/legacy/ # vendored ADA-F / LPA / operator-MPC backend (required at runtime)
│ ├── systems/ # ODESystem, built-in systems, CallableODESystem, registry
│ ├── forward/ # ForwardSolver, ForwardOptions
│ ├── operator/ # OperatorLearner, predict_step, predict_rollout
│ ├── mpc/ # MPCOptions, generic and surrogate MPC
│ ├── inverse/ # InverseSolver, InverseOptions, InverseParameter, data_gen
│ ├── workflows/ # run_forward, run_operator, run_mpc, run_inverse, run
│ └── utils/ # paths, metrics, plotting
├── examples/ # simple_api/, forward/, inverse/, operator/, mpc/
├── tests/ # runnable feature scripts (see Tests)
├── docs/ # Sphinx sources for the ReadTheDocs site
└── paper_artifacts/ # archived checkpoint + data for the paper's bioreactor results
The defaults used in the quick-start examples above are for illustration only — they do not reproduce the accuracy reported in the paper. Each benchmark's operator was trained with a system-specific network size and training-set size (the paper's operator hyperparameter table, Appendix A):
| System | N_p | n_seg | hidden | n_layers | n_train | epochs | batch_size |
|---|---|---|---|---|---|---|---|
| Lotka–Volterra | 21 | 20 | 256 | 3 | 4,000 | 2,000 | 128 |
| Triple-tank | 20 | 10 | 128 | 3 | 20,000 | 2,000 | 128 |
| CSTR | 24 | 25 | 192 | 3 | 20,000 | 2,000 | 256 |
| Fed-batch bioreactor | 30 | 50 | 128 | 3 | 50,000 | 2,000 | 128 |
To reproduce, e.g., the CSTR operator accuracy, pass these explicitly rather than relying on the quick-start defaults:
import adalib
system = adalib.get_system("cstr")
options = adalib.OperatorOptions(
basis="lpa",
n_train=20000, n_val=4000,
hidden=192, n_layers=3,
epochs=2000, batch_size=256,
work_dir="./runs/cstr_operator_paper",
)
result = adalib.run_operator(
system=system,
x0=[0.8, 0.5, 134.14, 130.0],
t_span=(0.0, 0.5),
options=options,
)
n_seg comes from the built-in system's segmentation, not from
OperatorOptions. Complete runnable scripts at these sizes exist for the
CSTR (tests/test_adalib_operator.py) and the triple tank
(tests/test_adalib_operator_triple_tank.py). Training at these sizes takes
much longer than the quick-start defaults.
The archived checkpoint, training configuration, and evaluation data behind
the paper's fed-batch bioreactor results are in
paper_artifacts/bioreactor_fig7_checkpoint/; see that directory's README for
the exact reproduction steps. The current FedBatchBioreactor sampling ranges
differ from the archived run, so retraining with the row above does not
reproduce those numbers exactly.
Forward simulation (Euler rigid body). The paper's 1,500-parameter
configuration is ForwardOptions(basis="adaf", N_p=10, n_seg=50, N_m=100, Nt_total=2500, gamma=0.9, epochs=5, adam_inner=100, use_lbfgs=True); see also
examples/forward/euler_forward.py. The forward scripts in tests/ use
smaller quick-start settings.
run_operator(CallableODESystem, …)) trains
physics-residual only — same philosophy as the built-in systems, no
reference trajectory is generated or fit against (adalib/mpc/ _generic_mpc.py: _sample_inputs + _train_lpa_operator_physics). The
physics target is evaluated via system.rhs(); a fully-vectorized numpy
call is attempted first (fast), falling back to a per-sample Python loop
with a one-time warning if the RHS uses scalar-only ops (float(u),
math.exp, builtin min/max on arrays) — use np.exp/np.minimum/
np.maximum instead for the fast path. Override the per-state residual
normalization via opts.res_scale={"state_name": scale} if needed.fedbatch_bioreactor system. Custom-system economic MPC is planned.opts.lpa_n_panels,
opts.lpa_max_order, opts.lpa_nt_seg if needed.PROBLEM, BASIS). Do not run two such workflows
with different systems concurrently in the same process.options.work_dir and are not shipped with the package; each user generates
them on first run.Released under the MIT License (see LICENSE). The vendored backend in
adalib/_vendor/legacy/ is covered by THIRD_PARTY_NOTICES.md.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.1.9 | 12 Oct 2026 | 14 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 19:00 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:74d2046846ef75d944909b33ee1ff23b4beb08bbfca53996d541453439ee3888