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.

Decomposition - Leslie-Gower Population Dynamics Example

The following example is based on the dynamics of a general predator-prey system described by Bazykin (1998) consisting of a set of ODEs.

dx0dt=(r1−a1x1−b1x0)x0dx1dt=(r2−a2x1x0)\begin{align} \frac{dx_0}{dt} &= (r_1-a_1x_1-b_1x_0)x_0 \tag{1}\\ \frac{dx_1}{dt} &= (r_2 - a_2\frac{x_1}{x_0}) \tag{2} \end{align}

Here, x0(t)x_0(t) represents the evolution of the prey population, while x1(t)x_1(t) represents the predator. For this example, the constant parameters include: the intrinsic growth rate of prey and predator, r1=r2=15r_1=r_2=\frac{1}{5}, the predation rate a1=15a_1=\frac{1}{5}, the intra-prey competition coefficient, b1=110b_1=\frac{1}{10}, and the predator food-scarcity (or Leslie Gower) coefficient, a2=1100a_2=\frac{1}{100}.

We will assume that the predator population evolution rate, r2−a2x1x0r_2-a_2 \frac{x_1}{x_0}, is unknown, and we would like to use a Neural Network to approximate the term. Written explicitly:

z(t)=r2−a2x1x0=fNN(x(t),θ)\begin{align} z(t) = r_2-a_2 \frac{x_1}{x_0} &= f_{NN}(x(t), \theta) \tag{3} \\ \end{align}

NOTE: This example uses the high-level HybridDAE wrapper, whose .fit() and .predict() methods orchestrate the full smoother, pretrain, train, and inference pipeline. To run each stage explicitly, call the stage functions directly as documented in the API reference.

Let’s start with the required imports

Defining a Problem

As in Four Tank Example, the Leslie Gower system should be defined as a class, inheriting from ProblemDefinition and implementing build_trajectory(), get_input_vars(), and get_ouput_vars(). In this example problem, we know the true dynamics of our problem (i.e. the r2−a2x1x0r_2 - a_2\frac{x_1}{x_0} dynamics), so the true_constraints() can be defined, allowing synthetic data generation and prediction comparison to the ground truth.

Defining the model’s mechanistic parameters discussed above.

Next, define the initial conditions for the data generation from the ground truth (since this is a known example). In this example, we will train for only one trajectory and thus require only one (x0,x1)(x_0, x_1) state variable IC pair.

Next, define the observed variable state dimension, x(t)x(t), the number of NN modeled variables, and z(t)z(t). There are no algebraic variables, y(t)y(t) in this problem.

The timespan over which the model should be solved.

Training discretization scheme. See Four Tank Example for more details.

Now we can define the problem instance

Generating the Data

With the problem defined, we can use the generate_data() function to generate the synthetic training data.

=== 1. Generating data ===
generate_data: ok / optimal

Step 1 - Defining the Neural Network

Before pretraining, the NN used to estimate the z(t)z(t) terms should be defined. SinDAE, comes equipped with a SimpleMLP class as an equinox MLP implementation, which we will be using for this example. To set up your own NN architecture, see Neural Network Utilities.

Step 2 - Configure the Pipeline Stages

The HybridDAE wrapper runs the full pipeline: the smoother (a data-fitting NLP with z(t)z(t) free), supervised pretraining, and the training solve. Here we select the decomposition method, which alternates Adam parameter updates with KKT-gradient back-solves through the DAE. The decomposition subproblems are solved with the chosen NLP solver (POUNCE by default), and their KKT systems with the linear solver (FERAL by default).

The training discretization can differ from the data-generation grid, so we update the problem’s nfe / ncp before fitting.

Step 3 - Fit the Hybrid Model (Decomposition)

Assemble the stage configs into a HybridDAE with method="decomposition" and call fit. The decomposition method has no single training termination (its per-step inner solves are recorded in model.history instead), which we plot below. Every intermediate stays reachable as an attribute (model.smoother_data, model.trained_data, model.history).

=== Building smoother for 1 trajectories (smooth_coef=1.0) ===
=== Fitting hybrid model (decomposition) ===
  Smoother: ok / optimal
=== Building decomp model for 1 trajectories (rank 0) ===
=== Training (300 steps) ===
  step   10  obj=7.0488e-02  data_fit=7.0485e-02  slack=2.14e-07  |grad|=2.76e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   20  obj=6.1504e-02  data_fit=6.1504e-02  slack=1.98e-08  |grad|=1.78e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step   30  obj=5.5119e-02  data_fit=5.5119e-02  slack=7.46e-08  |grad|=1.97e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   40  obj=4.7473e-02  data_fit=4.7473e-02  slack=1.38e-08  |grad|=1.27e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   50  obj=4.0141e-02  data_fit=4.0140e-02  slack=1.63e-07  |grad|=1.14e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   60  obj=3.3094e-02  data_fit=3.3094e-02  slack=9.14e-09  |grad|=8.39e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   70  obj=2.6812e-02  data_fit=2.6812e-02  slack=2.80e-08  |grad|=7.90e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   80  obj=2.1694e-02  data_fit=2.1694e-02  slack=4.69e-08  |grad|=6.59e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step   90  obj=1.7984e-02  data_fit=1.7983e-02  slack=9.40e-08  |grad|=4.98e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step  100  obj=1.5390e-02  data_fit=1.5389e-02  slack=9.48e-08  |grad|=4.28e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step  110  obj=1.4346e-02  data_fit=1.4346e-02  slack=2.97e-08  |grad|=7.37e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step  120  obj=1.6461e-02  data_fit=1.6461e-02  slack=6.37e-08  |grad|=1.34e-03  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.13
  step  130  obj=1.3805e-02  data_fit=1.3805e-02  slack=1.95e-08  |grad|=9.54e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.13
  step  140  obj=1.2754e-02  data_fit=1.2754e-02  slack=1.13e-08  |grad|=6.98e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step  150  obj=1.2004e-02  data_fit=1.2004e-02  slack=1.14e-08  |grad|=4.69e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.13
  step  160  obj=1.1412e-02  data_fit=1.1412e-02  slack=7.39e-09  |grad|=9.57e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step  170  obj=1.1064e-02  data_fit=1.1064e-02  slack=1.97e-08  |grad|=3.26e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.10
  step  180  obj=1.0775e-02  data_fit=1.0775e-02  slack=6.83e-09  |grad|=7.33e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.10
  step  190  obj=1.0546e-02  data_fit=1.0521e-02  slack=2.47e-06  |grad|=5.84e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.10
  step  200  obj=1.0312e-02  data_fit=1.0312e-02  slack=1.97e-08  |grad|=2.58e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step  210  obj=1.0145e-02  data_fit=1.0145e-02  slack=1.97e-08  |grad|=4.20e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step  220  obj=1.0036e-02  data_fit=1.0009e-02  slack=2.68e-06  |grad|=2.88e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.16
  step  230  obj=9.9511e-03  data_fit=9.9031e-03  slack=4.81e-06  |grad|=2.87e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.10
  step  240  obj=9.8191e-03  data_fit=9.8189e-03  slack=1.97e-08  |grad|=2.56e-05  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.10
  step  250  obj=9.8180e-03  data_fit=9.8178e-03  slack=1.81e-08  |grad|=3.55e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.10
  step  260  obj=1.1845e-02  data_fit=1.1844e-02  slack=1.37e-08  |grad|=1.80e-03  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.31
  step  270  obj=1.0186e-02  data_fit=1.0186e-02  slack=1.98e-08  |grad|=9.07e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.12
  step  280  obj=9.7995e-03  data_fit=9.7993e-03  slack=1.97e-08  |grad|=3.89e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.17
  step  290  obj=9.7802e-03  data_fit=9.7800e-03  slack=1.90e-08  |grad|=3.96e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
  step  300  obj=9.6929e-03  data_fit=9.6897e-03  slack=3.18e-07  |grad|=1.66e-04  lr=5.00e-03  slack_coef=1.0e+01  step_s=0.11
=== Training complete ===
Identifier                                  ncalls   cumtime   percall      %
-----------------------------------------------------------------------------
training                                         1    35.897    35.897  100.0
        ---------------------------------------------------------------------
        step                                   300    35.884     0.120  100.0
             ----------------------------------------------------------------
             get_values                        300     0.135     0.000    0.4
             grad_eval                         300     2.092     0.007    5.8
                       ------------------------------------------------------
                       back_solve              300     0.046     0.000    2.2
                       del_obj_del_rho         300     0.086     0.000    4.1
                       kkt_matrix              300     0.732     0.002   35.0
                       numeric_fact            300     0.826     0.003   39.5
                       vjp                     300     0.388     0.001   18.6
                       other                   n/a     0.013       n/a    0.6
                       ======================================================
             solve                             300    32.856     0.110   91.6
             update_mlp                        300     0.067     0.000    0.2
             update_nlp                        300     0.645     0.002    1.8
                       ------------------------------------------------------
                       build_extended_nlp        1     0.002     0.002    0.4
                       build_interface         300     0.500     0.002   77.5
                       other                   n/a     0.143       n/a   22.1
                       ======================================================
             other                             n/a     0.089       n/a    0.2
             ================================================================
        other                                  n/a     0.013       n/a    0.0
        =====================================================================
=============================================================================

=== Per Trajectory Metrics ===
RMSE: 
╒════════╤═══════════╤═══════════╤═══════════╕
│        │       x_0 │       x_1 │     NRMSE │
╞════════╪═══════════╪═══════════╪═══════════╡
│ traj_0 │ 0.0471514 │ 0.0677107 │ 0.0290642 │
├────────┼───────────┼───────────┼───────────┤
│ mean   │ 0.0471514 │ 0.0677107 │ 0.0290642 │
╘════════╧═══════════╧═══════════╧═══════════╛

Plot Results

<Figure size 1920x1440 with 0 Axes>
<Figure size 2400x900 with 2 Axes>
<Figure size 1200x900 with 1 Axes>
<Figure size 3000x1200 with 2 Axes>

Step 4 - Inference using Trained Model

Now that the hybrid model is trained, we can call model.predict() to predict trajectories given different initial conditions.

Accordingly, we can set new initial conditions for a new instance of the Leslie-Gower problem defined above. Other parameters including the number of discretization points, the number of collocation points, the time span, or the mechanistic parameters can also be altered before doing inference.

Since we know the ground truth dynamics for this problem, we can generate a ground truth trajectory to compare the inference result to. Of course, when making predictions on real problems, this is not usually possible. Here, the ground truth curve in intended to serve as a visual reference for hybrid model predictive capacity once trained.

A ground truth curve can be generated using the generate_data() function with noise_std set to 0 (for each dependent variable).

generate_data: ok / optimal

model.predict() embeds the trained network in the new problem and solves the inference NLP. It reuses the normalization statistics captured during fit (from the smoother stage), so the network is evaluated in the space it was trained in, and returns the predicted trajectories as an InstanceData object directly.

By default, predict enforces the NN equality as a hard constraint (slack_coef=0.0). If that leads to convergence difficulties, a small slack_coef adds l1 slack to loosen the constraint within reason. See Inference for more details.

=== Building inference model for 1 trajectories (slack_coef=1e-05) ===
=== Solving inference model ===
  Inference: ok / optimal

Plotting, similar to above.

<Figure size 1920x1440 with 0 Axes>
<Figure size 2400x900 with 2 Axes>
<Figure size 1200x900 with 1 Axes>
References
  1. Bazykin, A. D. (1998). Nonlinear dynamics of interacting populations. World Scientific.
  2. Lueg, L. R., Alves, V., Schicksnus, D., Kitchin, J. R., Laird, C. D., & Biegler, L. T. (2025). A simultaneous approach for training neural differential-algebraic systems of equations. arXiv Preprint arXiv:2504.04665. https://arxiv.org/abs/2504.04665