******************** Open-Loop Controller ******************** The :class:`~mgrowthctrl.controllers.open_loop.OpenLoopNeuralController` provides methods to learn time-dependent control inputs :math:`u(t)`. Example ======= We first define a one-species, one-resource consumer-resource model (CRM) and assign its parameters. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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: .. image:: ../_static/basic_open_loop_result.png :alt: Open-loop control predictions :align: center Finally, we can inspect the learned control input over time. .. code-block:: python 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: .. image:: ../_static/basic_open_loop_control_inputs.png :alt: Open-loop control inputs :align: center For a detailed tutorial on open-loop control of a fitted consumer-resource model, please see the tutorial :doc:`../tutorials/advanced_open_loop_control_fitting`. API Documentation ================= .. automodule:: mgrowthctrl.controllers.open_loop :members: :member-order: bysource :undoc-members: :show-inheritance: