Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

HybridDAE

sindae.hybrid_dae

The high-level fit/predict wrapper, and the primary way to use SiNDAE. One HybridDAE object runs the whole pipeline: fit(problem) solves the smoother, pretrains the network, and trains with the chosen method; predict(new_problem) embeds the trained network in a new problem and solves the inference NLP.

After fit, the training solve’s termination condition is on model.termination (None for the decomposition method, whose per-step inner solves are tracked in model.history), and any non-optimal solve raises a UserWarning. Every intermediate stays reachable: the solved smoother model (smoother_model), the normalization data (smoother_data), the solved training model (training_model), its extracted trajectories (trained_data), and the trained network (net). The stage functions (solve_smoother, pretrain_mlp, solve_simultaneous, train_decomp, solve_inference) remain the low-level escape hatch when you need stage-level control.

Usage

import jax
import numpy as np
import sindae as sd

jax.config.update('jax_enable_x64', True)

problem = sd.LeslieGowerProblem(nfe=40, ncp=3)
sd.generate_data(problem, noise_std=np.array([0.05, 0.05]), obs_every=4)

mlp = sd.SimpleMLP(
    in_size=problem.input_dim, out_size=problem.z_dim,
    widths=[16, 16], activations=[jax.nn.softplus] * 2,
    key=jax.random.PRNGKey(0),
)

model = sd.HybridDAE(
    method="simultaneous",             # or "decomposition"
    nlp_solver="pounce",               # "ipopt" / "cyipopt" selectable
    linear_solver="feral",             # decomposition KKT solver; "ma27" / "scipy"
    net=mlp,
    train=sd.SimultaneousConfig(reg_coef=1e-3),  # DecompConfig for "decomposition"
    solver_options=sd.SolverConfig(tol=1e-6, max_iter=1000),
)
model.fit(problem)                     # smoother -> pretrain -> train
print(model.termination)               # "optimal"

# Predict under new initial conditions
new_problem = sd.LeslieGowerProblem(ics=np.array([[1.2, 0.15]]), nfe=40, ncp=3)
pred = model.predict(new_problem, slack_coef=1e-5)

mu_hat = model.net                     # the trained SimpleMLP

# Persist the trained network and its scaler, then reload
model.save("mu_hat.eqx")
reloaded = sd.HybridDAE.load("mu_hat.eqx")
pred = reloaded.predict(new_problem, slack_coef=1e-5)   # scaler restored

# Export for a foreign optimization tool
model.export("mu_hat.json")            # plain-text bundle, no extra dependencies
model.export("mu_hat.onnx")            # ONNX graph (normalized) + scaler sidecar
model.export("mu_hat.onnx", scaled=True)  # scaler baked in: raw-in, raw-out graph
net_def = model.to_omlt()              # in-memory OMLT NetworkDefinition

save writes the network weights, its architecture, and the four normalization vectors the inference stage needs, so a reloaded model can predict right away or warm-start a fresh fit from the loaded weights. The stage configs and training trajectories are not persisted, so load is for resuming or serving a trained network, not for reproducing the original solve bit for bit.

save/load round-trip back into SiNDAE; export and to_omlt are a one-way handoff to another modeling tool. Both carry the scaler, so the network is evaluated in the space it was trained in. export(path) writes a file (.json for a dependency-free bundle of weights, activations, scaler, input bounds, and the ordered input/output variable names; .onnx for the graph plus a scaler sidecar) and needs the matching extra (pip install 'sindae[onnx]'). to_omlt() returns an omlt.neuralnet.NetworkDefinition whose inputs and outputs are the raw physical variables (the normalization rides along as an OMLT OffsetScaling), ready to drop into your own optimization model with an OMLT formulation; it needs pip install 'sindae[omlt]'. The ONNX graph itself stays in normalized space by default because OMLT applies the scaler separately; pass export(path, scaled=True) to bake the scaler into the graph as affine layers instead, giving a self-contained model that maps raw physical inputs to raw physical outputs in any ONNX runtime (the sidecar’s scaling field records which contract applies, so a consumer never double-applies the scaler).

The smoother stage is configured the same way when its defaults are not right, for example smoother=sd.SmootherConfig(smooth_coef=10.0) for noisier data. Pretraining always runs; pretrain=None means PretrainConfig() (200 epochs), and pretrain=sd.PretrainConfig(epochs=0) disables it. For partially observed problems (unmeasured states with no data anchor), pass unfix_io=False.

API reference

HybridDAE

class HybridDAE(
    method: str = 'simultaneous',
    nlp_solver: str = 'pounce',
    linear_solver: str = 'feral',
    net: Optional[SimpleMLP] = None,
    smoother: Optional[SmootherConfig] = None,
    pretrain: Optional[PretrainConfig] = None,
    train: Union[SimultaneousConfig, DecompConfig, None] = None,
    solver_options: Optional[SolverConfig] = None,
    unfix_io: bool = True,
)

scikit-learn-style facade over the SiNDAE training pipeline.

fit(problem) runs smoother -> pretrain -> train; predict(new_problem) runs inference with the trained network.

Stage configuration comes in as the same config dataclasses the stage functions use; cross-cutting choices (nlp_solver, linear_solver, solver_options, unfix_io) live only here, so no stage config can silently override them.

Parameters

Properties

Methods

fit

fit(
    problem: ProblemDefinition,
    metrics: Optional[list[str]] = None,
    tee: bool = False,
) -> 'HybridDAE'

Run the training pipeline on problem and return self.

Stages: solve the smoother, extract normalization data, pretrain the network on the smoother arrays, then train with the configured method (simultaneous NLP or decomposition loop). Any non-optimal solve raises a UserWarning; the training solve’s termination condition lands on self.termination.

Parameters

Returns

predict

predict(
    problem: ProblemDefinition,
    slack_coef: float = 0.0,
    solver_options: Optional[SolverConfig] = None,
    eval_metrics: Optional[list[str]] = None,
    tee: bool = False,
) -> InstanceData

Embed the trained network in problem and solve the inference NLP.

Normalization statistics are the ones the training stage consumed (self.smoother_data), so the network is evaluated in the space it was trained in. The solved model is kept on self.inference_model; a non-optimal solve raises a UserWarning.

Parameters

Returns

save

save(path) -> None

Serialize the trained network and its scaler to path.

Writes a one-line JSON manifest (architecture, activation names, the four normalization vectors from smoother_data, plus method and termination) followed by the Equinox leaf arrays. Reload with :meth:HybridDAE.load.

Only the network and scaler are persisted, not the stage configs or the training trajectories, so a loaded model can predict or warm-start a fresh fit but cannot reproduce the original solve bit-for-bit.

Parameters

load

load(cls, path: str, verbose: bool = False) -> 'HybridDAE'

Reconstruct a fitted :class:HybridDAE from a :meth:save file.

The returned wrapper can predict immediately (the scaler is restored on smoother_data as a :class:NormStats) or fit again to warm-start from the loaded weights. trained_data and the stage configs are not restored (they are not persisted); fit would use the default configs.

Parameters

Returns

export

export(path = None, format: Optional[str] = None, scaled: bool = False) -> str

Export the trained network to a file for a foreign optimization tool.

Unlike :meth:save (which round-trips back into SiNDAE), export is a one-way handoff. Two file targets, both carrying the scaler so the network is evaluated in the space it was trained in:

For an in-memory OMLT model (not a file), use :meth:to_omlt.

Parameters

Returns

to_omlt

to_omlt()

Build an in-memory OMLT model of the trained network.

Returns an omlt.neuralnet.NetworkDefinition with the normalization attached as an OffsetScaling (so the OMLT block’s inputs/outputs are the raw physical variables, not normalized ones) and the data-derived input bounds as its scaled_input_bounds. Feed it to an OMLT formulation (e.g. FullSpaceSmoothNNFormulation) inside your own optimization model. Needs the omlt extra.

Returns