2.0

Dynamic operability, a new NLP solver, brand new tools for design, models, and skills for coding agents

Victor Alves
Incoming Assistant Professor
Postdoctoral Fellow
Department of Chemical Engineering
July 23, 2026

Opyrability, and what is new in 2.0

Map inputs to reachable outputs, score operability, and invert targets back to designs.

New in 2.0

  1. Dynamic operability: funnels and the dynamic OI
  2. One-command install with the Pounce solver
  3. Design tools: ranking, MILP design, intensification
  4. Any model: Python, Pyomo, surrogates
  5. LLM and coding agent integration
  6. 147 tests on Python 3.11 to 3.14
opyrability 2.0

Adoption :)

37k+   PyPI downloads and counting, used across the northern and southern hemispheres.

opyrability 2.0

In the wild - many different applications!

Membrane reactor (JAX)
Direct air capture (AVEVA)
Air separation (UniSim)
Sludge gasification (Aspen)
Solid oxide electrolysis (IDAES)
Air cooling system (DWSIM)
Built and used by groups at WVU, CMU, universities in Brazil and across the world.
opyrability 2.0

1. Dynamic operability

Reachable-output funnels and the dynamic Operability Index

opyrability 2.0

From steady state to dynamics

Steady-state operability asks whether the desired outputs are reachable at all. Dynamic operability asks whether they are reachable within a finite number of steps, and how fast.

For xk+1=f(xk,uk)x_{k+1} = f(x_k, u_k),   yk=g(xk)\; y_k = g(x_k), the achievable output set at step kk is

AOSk(x0)={yk=g(xk)    ujAIS, j=0,,k1},dOI(k)=μ ⁣(AOSkDOS)μ(DOS)×100%\mathrm{AOS}^{k}(x_0) = \big\{\, y_k = g(x_k) \;\big|\; u_j \in \mathrm{AIS},\ j = 0,\dots,k-1 \,\big\}, \qquad \mathrm{dOI}(k) = \frac{\mu\!\left(\mathrm{AOS}^{k} \cap \mathrm{DOS}\right)}{\mu(\mathrm{DOS})} \times 100\%

Takeaway: Opyrability can now measure how operable a system is as it evolves in time, not only at steady state.
opyrability 2.0

One call

from opyrability import dynamic_operability
import numpy as np

AIS = [[-1, 1], [-1, 1]]
DOS = [[-2, 2], [-2, 2]]
model = {'A': 0.9*np.eye(2),
         'B': np.eye(2),
         'C': np.eye(2)}

results = dynamic_operability(model,           # LTI dict or step fn
                              x0=np.zeros(2),  # initial state
                              AIS_bound=AIS,   # input set
                              DOS=DOS,         # target outputs
                              k_max=4)         # horizon
print(results['dOI'])  # [25. 90.25 100. 100.]
opyrability 2.0

Example: membrane reactor for methane aromatization

Upgrade methane to benzene and hydrogen; the membrane pulls H2\mathrm{H_2} out to push conversion past equilibrium.

2CH4C2H4+2H22\,\mathrm{CH_4} \rightleftharpoons \mathrm{C_2H_4} + 2\,\mathrm{H_2}

3C2H4C6H6+3H23\,\mathrm{C_2H_4} \rightleftharpoons \mathrm{C_6H_6} + 3\,\mathrm{H_2}

AIS: tube and shell flowrates · DOS: benzene and H2 mole fractions · AOS: what the reactor can actually reach
opyrability 2.0

PDE, distributed model

Transient mole balances on the molar holdups of each species (tube and shell) along the reactor length. Two equilibrium-limited reactions, with membrane H2\mathrm{H_2} removal shifting them forward (Le Chatelier):

r1=(1vb)ηk1CCH4K1CCH42CC2H4CH22K1CCH42,r2=(1vb)ηk2CC2H4K2CC2H43CC6H6CH23K2CC2H43r_1 = -(1-v_b)\,\eta\,k_1\,C_{\mathrm{CH_4}}\,\frac{K_1 C_{\mathrm{CH_4}}^{2} - C_{\mathrm{C_2H_4}} C_{\mathrm{H_2}}^{2}}{K_1 C_{\mathrm{CH_4}}^{2}}, \qquad r_2 = -(1-v_b)\,\eta\,k_2\,C_{\mathrm{C_2H_4}}\,\frac{K_2 C_{\mathrm{C_2H_4}}^{3} - C_{\mathrm{C_6H_6}} C_{\mathrm{H_2}}^{3}}{K_2 C_{\mathrm{C_2H_4}}^{3}}

Jj=36QαjπDtΔz(Pt,j1/4Ps,j1/4)Sieverts-type membrane permeationJ_j = \frac{36\,Q}{\alpha_j}\,\pi D_t\,\Delta z \left( P_{t,j}^{1/4} - P_{s,j}^{1/4} \right) \qquad \text{Sieverts-type membrane permeation}

Discretized into N=20N=20 elements, 4 tube + 4 shell species: a 160-state model. Opyrability needs only a one-minute step, dma_mr_cocurrent_step(x, u, d) returning (x_next, y).

opyrability 2.0

Reachable-output funnel (interactive)

AOS per minute (benzene and H2) with Monte Carlo trajectories, scored against the DOS; color shows the dynamic OI (%). Drag to rotate · open full screen
opyrability 2.0

HYPER: a cyber-physical fuel-cell and gas-turbine system

AIS: electricity load, cold-air bypass · DOS: turbine speed, cathode airflow

Identified 2×22\times2 first-order model:

G(s)=[0.25s+0.2250.065s+0.1250.22s+0.691.43s+1.43]G(s) = \begin{bmatrix} \dfrac{-0.25}{s+0.225} & \dfrac{-0.065}{s+0.125} \\[1em] \dfrac{-0.22}{s+0.69} & \dfrac{-1.43}{s+1.43} \end{bmatrix}

ZOH-discretized (Ts=1T_s=1 min) to a four-state xk+1=Axk+Bukx_{k+1}=Ax_k+Bu_k.

Schematic, model and equations from San Dinh's PhD dissertation (WVU).
opyrability 2.0

HYPER: reachable-output funnel (interactive)

AOS per minute (turbine speed and cathode airflow) over 20 minutes, scored against the DOS; color shows the dynamic OI (%). Drag to rotate · open full screen
opyrability 2.0

Choosing a dOI evaluation?

Membrane-reactor funnel by three methods.

Same funnel, three ways to compute it:

  • exact linear projection for LTI systems (Minkowski sums, affine maps)
  • nonlinear vertex projection in state space
  • n-step simulation for any model

dynamic_operability picks one by state dimension and model type, or you force it with method=.

opyrability 2.0

Disturbance scenarios and robust operability

Build one funnel per distrubance scenario, then take their intersection: the outputs guaranteed reachable no matter which disturbance hits the process.

scenario_results = dynamic_operability_scenarios(
    make_step,            # step(x, u) for a feed d
    make_initial_state,   # steady state for feed d
    AIS_bounds,
    scenarios={'Low quality well (92% CH4)':  92.0,
               'High quality well (96% CH4)': 96.0},
    DOS=DOS_bounds, k_max=6, engine='plotly')

Empty for two minutes, then the robust region grows: adjusting inlet flowrates compensates for feed quality.

Two scenario funnels and their robust intersection (blue). Drag to rotate.
opyrability 2.0

HYPER under disturbance scenarios (interactive)

Each colored outline is one of the four corner disturbances (fuel valve and hot-air bypass at their 95% bounds); the blue intersection is the region reachable under all of them, and it grows as the inputs act. Drag to rotate · HYPER dynamic example
opyrability 2.0

2. One command to install

opyrability 2.0

Install and go

Before, running the inverse mapping meant installing IPOPT through conda first.

Now, one command:

pip install opyrability

That is it. It includes everything you need, the solver included. cyipopt stays optional, only for method='ipopt'.

opyrability 2.0

Powered by Pounce

Pounce is a pure-Rust interior-point NLP solver from John Kitchin's group at CMU, now the default for Opyrability's inverse mapping and design tools. Why is this important?

  • installs as prebuilt wheels: nothing to compile, no third-party binaries, no MUMPS, no licensed HSL to track down
  • bundles FERAL, its own sparse symmetric-indefinite linear solver with certified inertia, so the interior-point steps stay reliable without an external solver
  • one pip install gives a fast, self-contained solver on Windows, macOS, and Linux
kitchingroup.cheme.cmu.edu/pounce
opyrability 2.0

Pounce in action

Pounce is the default, so nothing changes in your call:

fDIS, fDOS, msg = nlp_based_approach(model,        # process model
                                     DOS,          # target outputs
                                     resolution,   # grid density
                                     u0,           # initial guess
                                     lb, ub)       # input bounds
# method='pounce' by default; 'ipopt' still available
opyrability 2.0

3. Design decisions

opyrability 2.0

Rank competing designs

Score two or more designs by their Operability Index, from the output or the input side, and see which is more operable before committing.

from opyrability import rank_designs

ranking = rank_designs(models={'Tall':    model_a,   # {label: model}
                               'Compact': model_b},
                       AIS_bound=AIS,                 # input set(s)
                       DOS_bound=DOS,                 # target outputs
                       resolution=[10, 10],           # grid density
                       perspective='outputs')         # or 'inputs'
opyrability 2.0

Inverse mapping and intensification (NLP)

(P1) minuk j=1n ⁣(yj,kyj,kyj,k)2  s.t. M, umin ⁣uk ⁣umax\text{(P1)}\ \min_{u^*_k}\ \sum_{j=1}^{n}\!\Big(\tfrac{y_{j,k}-y^*_{j,k}}{y_{j,k}}\Big)^{2}\ \ \text{s.t. }M,\ u^{\min}\!\le u^*_k\!\le u^{\max}

P1: feasible inputs for the target (the DIS)

(P2) minu PI(u)  s.t. uDISPI, yDOSPI\text{(P2)}\ \min_{u^*}\ \mathrm{PI}(u^*)\ \ \text{s.t. }u^*\in DIS_{\mathrm{PI}},\ y^*\in DOS_{\mathrm{PI}}

P2: intensify by a metric PI over the feasible set

(P3) minuU PI(u)  s.t. uk ⁣ ⁣arg minukUk,ykDOS ⁣(P1)\text{(P3)}\ \min_{u\in\mathbb{U}}\ \mathrm{PI}(u)\ \ \text{s.t. }u^*_k\in\!\!\operatorname*{arg\,min}_{u^*_k\in\mathbb{U}_k,\,y_k\in DOS}\!\text{(P1)}

P3: bilevel; same design as P1 then P2
Formulations: P2 · P3 in the documentation.
fDIS, fDOS, msg = nlp_based_approach(
    model,                # process model
    DOS,                  # target outputs
    resolution,           # grid density
    u0, lb, ub,           # guess, bounds
    problem='P2',         # 'P1' / 'P2' / 'P3'
    PI_target=objective)  # metric to intensify
Push a design toward a tighter operating point, not just check that it works.
opyrability 2.0

Optimal modular design (MILP)

The multimodel polytopes turn the search for an optimal modular design into a single mixed-integer linear program, solved once per layer instead of many nonlinear solves.

u_opt, y_opt, phi, *_ = milp_based_approach(
    model,             # process model
    AIS_bound=AIS,     # input bounds
    PI_target=cost,    # metric to minimize
    DOS_bounds=DOS,    # target outputs
    AIS_resolution=5)  # grid density
One LP per layer, not many nonlinear solves: fast and mixed-integer linear.
opyrability 2.0

4. Model-agnostic mapping

opyrability 2.0

First-principles, process simulator, algebraic/equation-oriented, and/or ML surrogate

Opyrability maps whatever you can evaluate:

  • a plain Python function
  • an equation-oriented Pyomo model
  • a data-driven/ML surrogate (scikit-learn)

The plots show operability analyses from several modeling environments, all through the same API.

Pyomo model
Python function
GP surrogate
Design ranking (maybe multiple models?!)
opyrability 2.0

ML surrogates under uncertainty

Fit a Gaussian process surrogate, then propagate its uncertainty through the dynamic analysis.

For a CSTR under a GP-NARX surrogate:

dOI=45.3%\mathrm{dOI} = 45.3\%

+3σ: 51.7%, −3σ: 38.5%.
AOS is now a distribution; the mean and plus/minus three sigma funnels bound it.
Drag to rotate · open full screen
opyrability 2.0

5. LLM and coding agent integration

opyrability 2.0

Drive it with your coding agent

Opyrability ships with OPYRABILITY.md: one file that teaches an AI coding assistant the vocabulary, function signatures and best practices, the expected model (M) syntax, when to use each function, and the mistakes to avoid.

🧑‍💻 💬 </>
You ask; Claude Code, Codex, or Copilot reads OPYRABILITY.md, writes the model, and runs the operability analysis, from inverse maps to intensified designs.
opyrability 2.0

6. Documentation and testing

opyrability 2.0

Documentation

  • every function in the API reference ships with a runnable example
  • a gallery of worked case studies, from the shower problem to membrane reactors
  • a new section on the dynamic methods

Read the documentation and the operability fundamentals.

# every API page runs code like this
from opyrability import (
    multimodel_rep, OI_eval)

AOS = multimodel_rep(model, AIS,
                     resolution=[10, 10])
OI = OI_eval(AOS, DOS)   # -> 60.2 %
opyrability 2.0

Tested across multiple Python versions (3.11 - 3.14)

147

automated tests, significantly more when compared to the previous version 1.4.9

Run in continuous integration on Python 3.11, 3.12, 3.13, and 3.14, and on every pull request. They cover:

  • dynamic operability and funnels
  • design ranking and MILP design
  • Pyomo and implicit mapping
  • polytope geometry
opyrability 2.0

Contributing

Opyrability is open source and welcomes contributions. It will always be that way.

  • Report a bug or request a feature: open an issue on GitHub
  • Contribute code or a case study: fork the repo, branch, and open a pull request
  • CI runs the full test suite on your pull request across Python 3.11 to 3.14
github.com/CODES-group/opyrability
opyrability 2.0

Upgrading from 1.4.x

  • the default solver is now Pounce; for the IPOPT backend, pass method='ipopt' and install cyipopt from conda
  • Python 3.9 and 3.10 are no longer supported (both reached EOL); the minimum is 3.11
  • the conda package is retired; install with pip
opyrability 2.0

Opyrability 2.0 at a glance

  1. Dynamic operability: funnels + dOI, one call
  2. Pounce default solver, pure pip install
  3. rank designs, MILP based approach
  4. P1 / P2 / P3 inverse mapping s.t. PI
  5. Pyomo and ML surrogate models support
  6. OPYRABILITY.md agent skill
  7. 147 tests, Python 3.11 to 3.14

Docs · Code · Paper

pip install opyrability
opyrability 2.0

Thanks

Development began in the CODES Group at West Virginia University, with San Dinh and under Fernando V. Lima, and continues at Carnegie Mellon University. Pounce is the work of John Kitchin and his group.

opyrability 2.0