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.

Partial Observation - Fed-Batch Bioreactor

Real processes are rarely fully instrumented. For example, in a fed-batch fermentation, one might have online probes for biomass XX and substrate SS, while product PP requires an offline assay and the volume is inferred from the feed schedule. This notebook shows how to fit the hybrid model when only some of the states are measured, and how the trained model reconstructs the states you did not measure.

The system and the learned term are the same as in Importing Measured Data: a four-state fed-batch bioreactor (biomass XX, product PP, substrate SS, volume VV) whose specific growth rate μ\mu is replaced by a neural network of all four states. Here we assume only XX and SS are measured.

Two ideas are demonstrated:

  1. The observation model. get_obs_vars selects which quantities are compared against data, independently of get_input_vars (the states fed to the network). The number of measured channels (obs_dim) can be smaller than the number of states.

  2. Anchoring unmeasured states. A state that is never measured has no data to pin it down. We keep its known initial charge fixed (unfix_io=False) so the dynamics can reconstruct the rest of its trajectory.

The imports are the same as the previous example.

Defining the problem

The problem is the same fed-batch bioreactor (See Importing Measured Data), with the measured states changed from (X,P,S,V)(X, P, S, V) to only (X,S)(X, S). Accordingly, override get_obs_vars to return only the measured states, biomass XX (index 0) and substrate SS (index 2). get_input_vars still returns all four states, because the network is a function of the full state, but we lack measurements for every state. The constructor is given obs_dim=2 to match the two measured channels.

Step 1 - Load the measured channels

We load fedbatch_measurements.csv (created by generate_fedbatch_data.py, as in the previous example), but keep only the biomass and substrate columns as the data used for fitting. The product and volume columns are set aside as a held-out reference so we can check the reconstruction at the end.

Loaded 3 batches: measuring ['X', 'S'] (obs_dim=2)
Network still takes all four states as input (input_dim=4)

We attach the measurements to a problem instance with obs_dim=2, and supply the known initial charge of each batch.

Step 2 - Anchor the unmeasured states

With full observation the solver is free to adjust every state to fit its data. The default unfix_io=True even lets the measured initial values float so noisy data can be denoised. Here product PP and volume VV are never measured, so they have nothing to anchor them: VV in particular is only constrained by dV/dt=FdV/dt = F, which fixes its slope but not its level. Left free, these states drift and the solve diverges.

The remedy is to keep the known initial charge fixed by passing unfix_io=False to HybridDAE, which applies it to both the smoother and the training solve. Each unmeasured state is then pinned at t=0t=0 and reconstructed forward by the dynamics.

=== Building smoother for 3 trajectories (smooth_coef=10.0) ===
  Smoother: warning / maxIterations
/var/folders/bd/5y876_fs4td3chn71x256drh0000gn/T/ipykernel_41302/2069899238.py:30: UserWarning: HybridDAE smoother solve terminated with 'maxIterations' (not optimal)
  model.fit(problem, metrics=["rmse"])
=== Building simultaneous GBM model for 3 trajectories ===
=== Solving simultaneous GBM model (pounce, L-BFGS) ===
  pounce: ok / optimal
=== Simultaneous solve complete ===
=== Per Trajectory Metrics ===
RMSE: 
╒════════╤═══════════╤══════════╤═══════════╕
│        │       x_0 │      x_1 │     NRMSE │
╞════════╪═══════════╪══════════╪═══════════╡
│ traj_0 │ 0.0639192 │ 0.47079  │ 0.0279467 │
├────────┼───────────┼──────────┼───────────┤
│ traj_1 │ 0.0614122 │ 0.567883 │ 0.0568993 │
├────────┼───────────┼──────────┼───────────┤
│ traj_2 │ 0.0745706 │ 0.470892 │ 0.0388596 │
├────────┼───────────┼──────────┼───────────┤
│ mean   │ 0.066634  │ 0.503188 │ 0.0412352 │
╘════════╧═══════════╧══════════╧═══════════╛

The fit to the two measured states, and the recovered growth rate against the Monod curve (which we know only because this is a synthetic study):

<Figure size 4800x2700 with 12 Axes>
<Figure size 1500x1200 with 1 Axes>

Step 3 - Reconstruct the unmeasured states

Product PP and volume VV were never shown to the model, yet the trained hybrid model produces a full trajectory for them. Comparing those reconstructed trajectories against the held-out columns confirms they are recovered accurately, purely from the measured channels, the mechanistic structure, and the fixed initial charge.

<Figure size 3300x1200 with 2 Axes>
P: reconstruction RMSE vs held-out = 0.044
V: reconstruction RMSE vs held-out = 0.116

Summary

  • get_obs_vars defines the observation model independently of the network inputs, so obs_dim (here 2) can be smaller than the number of states (4).

  • An unmeasured state needs an anchor. Passing unfix_io=False keeps the known initial charge fixed, which is enough for the mechanistic dynamics to reconstruct product and volume from the measured biomass and substrate.

  • The learned growth rate and the reconstructed states are both recovered accurately from a partial dataset.