[1]:
import numpy as np
import x3cflux

Understanding the Simulator Object

The simulator object is the core of 13CFLUX3. It redirects atomic performance critical computations, such as forward simulations and its derivative, to highly performant C++ code. Building on these atomic computations, higher level routines, such as optimization or statistics, are available via Python. This guide introduces how to best create simulator objects and gives a quick overview of its features.

Creating simulator objects

Due to technical reasons, creating a simulator does not directly translate to instantiation of a class and requires calling special functions which are handed the necessary data and compile the simulator. This may sound complicated, but is very straightforward in practice and combines incredible performance with both flexibility and convenience. This tutorial explains the different ways of creating a simulator object and when to use which.

The standard way: FluxML-defined model

In the most simple case, network and labeling data are already encoded in the XML-based modeling language FluxML. We encourage using FluxML to obtain reproducible models. A model can be created by a single function call supplying the location of the FluxML file and the name of the FluxML configuration.

[2]:
simulator = x3cflux.create_simulator_from_fml("spiralus.fml", "ms+uptake")

FluxML files can store labeling data from multiple experiments. To create a simulator from multiple experiments with 13CFLUX, a list of the corresponding configuration names is supplied instead of single one.

[3]:
simulator = x3cflux.create_simulator_from_fml("spiralus.fml", ["ms+uptake", "ms"])

The flexible way: Programming-defined model

Using the Python interface of 13CFLUX, metabolic network including atom transitions and isotope labeling experiments can be created programmatically. This can be used for contextual modification of the metabolic network and or generation of in-silico labeling experiments without needing to edit XML code. There are two ingredients necessary to define labeling experiments from code: an x3cflux.NetworkData object for metabolites and reactions (including atom transitions) and one or multiple x3cflux.MeasurementConfiguration objects for labeling experiment specification and data.

FluxML files can be read in as data objects, too. By

[4]:
parser = x3cflux.FluxMLParser()
data = parser.parse("spiralus.fml")

a FluxML file is parsed into a data object containing metabolic network (as network data object) and all labeling experiments (as configuration objects) specified in the file. Now, a simulator can be created by

[5]:
simulator = x3cflux.create_simulator_from_data(data.network_data, data.configurations[0])

Similarly, both objects supplied to the create_simulator_from_data function can be replaced by self-created or modified ones.

The special ways: Application-specific convenience functions

13CFLUX provides two special create_simulator_* functions. They are supposed to streamline certain standard applications which require juggling with complicated data structures to reduce repetitive code and errors.

The first application is a very common theoretical task, in which perturbed data is generated to estimate parameters from it.

[6]:
perturbed_data = x3cflux.compute_perturbed_measurements(simulator, [0.6, 1.0], num_samples=1)
simulator = x3cflux.create_simulator_from_measurements(perturbed_data, simulator)

The second application is based on experimental design. Given a set of mixtures for some of the input tracers, a simulator modeling experiments for arbitrarily many different tracer mixtures is created.

[7]:
mixtures = x3cflux.parse_substrate_mixture_collections("mixture.xml")
substrate_mixtures, _ = x3cflux.compute_mixture_samples(mixtures, 10)
simulator = x3cflux.create_simulator_from_inputs(simulator.network_data,
                                                 simulator.configurations[0],
                                                 substrate_mixtures)

Parametrization

When speaking about parameters, the simulator’s main functionalities all take free net flux/exchange flux/pool size parameters, i.e. vectors from the solution spaces of the metabolic steady state equation \(\mathbf{S} \cdot \nu = \mathbf{0}\) and other user-supplied equality constraints. The choice of which parameters are free can be controlled by adding variables to the <simulation> section of the FluxML document or by supplying a list of x3cflux.ParameterEntries to x3cflux.MeasurementConfiguration. When supplying none or less than the systems DOF, free parameters will be selected automatically.

With a simulator at hand, parameter names can be easily accessed via

[8]:
simulator.parameter_space.free_parameter_names
[8]:
['q.n', 'u.n']

If supplied in the FluxML file or the x3cflux.MeasurementConfiguration, pre-computed configurations of free parameters can be obtained by

[9]:
params = x3cflux.get_parameters(simulator.parameter_space, simulator.configurations[0].parameter_entries)

If the values of all parameters are desired, they can be computed by

[10]:
list(zip(simulator.parameter_space.parameter_names, simulator.parameter_space.compute_parameters(params)))
[10]:
[('u.n', np.float64(1.0)),
 ('p.n', np.float64(0.4)),
 ('q.n', np.float64(0.6)),
 ('v.n', np.float64(0.4)),
 ('w.n', np.float64(0.4)),
 ('r.n', np.float64(0.6)),
 ('h_ouz.n', np.float64(0.6)),
 ('g_ouz.n', np.float64(0.4)),
 ('f_out.n', np.float64(0.4)),
 ('u.x', np.float64(0.0)),
 ('p.x', np.float64(0.0)),
 ('q.x', np.float64(0.0)),
 ('v.x', np.float64(0.0)),
 ('w.x', np.float64(0.0)),
 ('r.x', np.float64(0.0)),
 ('h_ouz.x', np.float64(0.0)),
 ('g_ouz.x', np.float64(0.0)),
 ('f_out.x', np.float64(0.0))]

Functionalities

The simulator object provides functionality to

  • Compute labeling simulations and its derivative (Jacobian)

  • Compute residuals to measurement data with exact first order (gradient) and approximated second order derivative (linearized hessian)

which is the basis of all computational procedures from 13C-MFA.

[11]:
simulator.compute_measurements(params)
simulator.compute_jacobian(params)
simulator.compute_loss(params)
simulator.compute_loss_gradient(params)
simulator.compute_linearized_hessian(params)
[11]:
array([[ 37263.10013717, -22357.8600823 ],
       [-22357.8600823 ,  13414.71604938]])

Forward simulation, residual computation and residual gradients are capable of parallel computation for multiple parameter vectors.

[12]:
many_params = np.column_stack((params, params))
simulator.compute_loss(many_params)
[12]:
array([12022.71022298, 12022.71022298])

Also, all functions are available for single and multiple configurations (when modeling multiple experiments). For multiple configurations, the above-mentioned functions provide special algorithms to speed up computations and generates combined output (e.g. compute_loss will return the sum of losses over all experimental datasets). Special compute_multi_... (jacobians, losses, loss_gradients, linearized_hessians)functions on the other hand will return lists with the individual results of each experiment.

[13]:
x3cflux.create_simulator_from_fml("spiralus.fml", ["ms+uptake", "ms"]).compute_multi_losses(params)
[13]:
[1.3096323621833204e-28, 1.3096323621833204e-28]

The solution space is typically restricted by linear inequality constraints (e.g. from bounds on all parameters), given by \(\mathbf{C} \cdot \theta \leq \mathbf{d}\). Matrix and vector from this equation are accessible via

[14]:
ineq_sys = simulator.parameter_space.inequality_system
ineq_sys.matrix, ineq_sys.bound
[14]:
(array([[-1.,  0.],
        [ 0., -1.],
        [ 1.,  0.],
        [ 0.,  1.],
        [ 1., -1.],
        [ 1., -1.],
        [ 1., -1.],
        [ 1., -1.],
        [ 1., -1.],
        [ 1., -1.]]),
 array([ 0.,  0.,  5.,  5.,  0.,  0.,  0., -0.,  0.,  0.]))

It is worth mentioning, that by default all above-mentioned functionalities require params to perfectly fulfill these inequality constraints. As computation might become mathematically unstable, it is however possibly to set a threshold, by which constraints may be violated:

[15]:
simulator.parameter_space.constraint_violation_tolerance = 1e-6

Access of internal state

The simulator object allows for easy access of internal states, such as parameter settings (simulator.parameter_space), reduced network (simulator.network) and labeling system builder (simulator.builder). By accessing the internal states, additional information about the inner working can be retrieved. Read the API for more in-depth understanding of how these classes store information. For technical reasons, each of these states map to slightly different classes depending on the characteristics of the experiment. E.g. in our current example, parameter settings are stored in the type

[16]:
simulator.parameter_space
[16]:
<x3cflux.lib.core.StationaryParameterSpace at 0x701a29902030>

For INST-MFA, on the other hand, simulator.parameter_space is an instance of x3cflux.NonStationaryParameterSpace.