Open-Loop Controller
The OpenLoopNeuralController provides methods to learn time-dependent control inputs \(u(t)\).
Example
We first define a one-species, one-resource consumer-resource model (CRM) and assign its parameters.
from mgrowthctrl.models import CRModel, CRModelParams
from mgrowthctrl.models.base import ModelNames
params = CRModelParams.from_shapes(n=1, m=1)
names = ModelNames(["biomass"], ["substrate"])
model = CRModel(names=names, params=params, backend="torch")
model.r[0, 0] = 0.5
model.a[0, 0] = 1.0
model.k[0] = 0.01
The controller is trained to reach a target final biomass while penalizing control effort.
import numpy as np
import torch
torch.manual_seed(42)
np.random.seed(42)
def steering_loss(trajectory, controller, model):
final_biomass = trajectory[-1, 0]
target = torch.tensor(1e1, dtype=torch.float32)
error = (torch.log(final_biomass + 1e-6) - torch.log(target + 1e-6)) ** 2
steps = trajectory.shape[0]
times = torch.linspace(0, controller.total_time, steps, dtype=torch.float32)
t_norm = (times / controller.total_time).unsqueeze(1)
U_tensor = controller.net(t_norm)
cost = 1e-3 * U_tensor.pow(2).mean()
return error + cost, U_tensor
Next, we initialize the open-loop neural controller.
from torch import nn
from mgrowthctrl.controllers import OpenLoopNeuralController
controller = OpenLoopNeuralController(
total_time=100,
n_species=model.n,
n_resources=model.m,
s_idx=[0],
criterion=steering_loss,
hidden_dims=[5, 5, 5],
activation=nn.ELU(),
)
We define a logging function, set the initial condition, and train the controller.
def log_fn(model, epoch, loss, U_trajectory, sol_tensor):
if epoch % 1 == 0:
actual = sol_tensor[-1, 0].item()
print(
f"Epoch {epoch:03d} | Loss: {loss.item():.4f} "
f"| Target: {1e1:.1e} | Actual: {actual:.1e}"
)
y0 = torch.tensor([0.1, 10.0], dtype=torch.float32)
t_eval = np.linspace(0, 100, 100)
t_span = (0, 100)
controller.fit(model, y0, t_eval, t_span, epochs=100, lr=1e-2, log_fn=log_fn)
We visually compare the uncontrolled and controlled biomass trajectories.
import matplotlib.pyplot as plt
uncontrolled_sol = model.predict(y0.numpy(), t_eval)
controlled_sol = controller.simulate(model, y0, t_eval, t_span)
fig, ax = plt.subplots(figsize=(6, 3.2))
ax.plot(uncontrolled_sol.t, uncontrolled_sol.y[0], label="Uncontrolled")
ax.plot(controlled_sol.t, controlled_sol.y[0], label="Controlled")
ax.scatter([100], [1e1], color="red", s=150, marker="*", label="Target", zorder=10)
ax.set_xlabel("Time (h)")
ax.set_ylabel("Biomass")
ax.legend()
plt.tight_layout()
plt.savefig("basic_open_loop_result.png")
plt.show()
We observe that the controlled dynamics reaches the desired concentration:
Finally, we can inspect the learned control input over time.
raw_u = controller.get_input_history(t_eval, model).detach()
fig, ax = plt.subplots(figsize=(6, 3.2))
ax.plot(t_eval, raw_u[:, 0], label="Substrate input")
ax.set_xlabel("Time (h)")
ax.set_ylabel("Injection rate (mM/h)")
ax.legend()
plt.tight_layout()
plt.savefig("basic_open_loop_control_inputs.png")
plt.show()
Here’s what the result looks like:
For a detailed tutorial on open-loop control of a fitted consumer-resource model, please see the tutorial Advanced: Open-Loop Control of Microbiome Dynamics.
API Documentation
- class mgrowthctrl.controllers.open_loop.OpenLoopNeuralController(
- total_time: float,
- n_species: int,
- n_resources: int,
- *,
- hidden_dims: Sequence[int] | None = None,
- activation: Module = Tanh(),
- criterion: Callable[[Tensor, BaseController, BaseODEModel], Tuple[Tensor, Any]] | None = None,
- injection_only: bool = True,
- x_idx: Sequence[int] | None = None,
- s_idx: Sequence[int] | None = None,
- target_substrate_idx: int | None = None,
Bases:
BaseControllerOpen-Loop Neural Controller (Time-Dependent Policy).
This controller learns a fixed schedule of interventions u(t) based solely on the current time t. It does not observe the current biological state (X, S) during execution.
This is analogous to a “blind archer” or a pre-calculated drug dosing schedule.
The controller outputs positive injection rates (via Softplus) which are added directly to the ODE derivatives:
dX/dt += u_x(t) (Probiotic injection)
dS/dt += u_s(t) (Nutrient/Prebiotic injection)
- Parameters:
total_time (float) – The maximum time (T_final) of the experiment. Inputs are normalized to the range [0, 1] as t_norm = t / total_time.
n_species (int) – Number of species in the system (dimension of X).
n_resources (int) – Number of substrates in the system (dimension of S).
hidden_dims (Sequence[int], optional) – Architecture of the neural network (neurons per hidden layer). Default: [16, 16].
activation (nn.Module) – Activation function for hidden layers.
criterion (LossFunction, optional) – The objective function used for training. Signature: (trajectory, controller, model) -> (loss, aux_data).
injection_only (bool) – If True, applies Softplus to the network output so controls are nonnegative.
x_idx (Sequence[int], optional) – Indices of species (X) to control (add/inject). Default is None (control no species).
s_idx (Sequence[int], optional) –
Indices of substrates (S) to control (add/inject).
If None: controls ALL substrates.
If []: controls NO substrates.
If [i, j]: controls specific substrates i and j.
target_substrate_idx (int, optional) – Convenience argument. If s_idx is None and this is set, the controller controls only this substrate index.
- net
The neural network mapping time t to control signal u
- forward(
- model: BaseODEModel,
- t: Tensor,
- X: Tensor,
- S: Tensor,
Calculates control signal at time t.
- Parameters:
model (BaseODEModel) – The metadata wrapper for the system (used for dimensions, etc.).
t (torch.Tensor) – Current simulation time.
X (torch.Tensor) – Current biological state (Ignored by OpenLoopNeuralController, but required by API).
S (torch.Tensor) – Current biological state (Ignored by OpenLoopNeuralController, but required by API).
- Returns:
dXc (torch.Tensor) – Additive change to biomass derivatives (same shape as X).
dSc (torch.Tensor) – Additive change to substrate derivatives (same shape as S).
- get_input_history(trajectory: Tensor, model: BaseODEModel) Tensor
Reconstructs the full control schedule u(t) for visualization.
Since this is an Open Loop controller, the schedule is fixed and independent of the actual bacterial state trajectory.
- Parameters:
trajectory (torch.Tensor) – Shape (Steps, State_Dim). Used to determine the time grid resolution.
- Returns:
U_tensor – Shape (Steps, Output_Dim). The control signal at each time step.
- Return type:
torch.Tensor