Particle-COMETS: From dynamic FBA to spatial simulations with particles¶

Eran Agmon, University of Connecticut

This notebook is dedicated to developing a particle-COMETS simulation step by step using Vivarium. Each section of this notebook incrementally adds layers to our model, illustrating the different computational methods and how they integrate.

In [1]:
import numpy as np
from attr import dataclass
# spatio-flux's customized Vivarium
from spatio_flux import SpatioFluxVivarium, render_path

Make a Vivarium¶

view available types and processes

In [2]:
# make a fresh vivarium
vi = SpatioFluxVivarium()
In [3]:
# view the available types
vi.get_types()
Out[3]:
['',
 'interval',
 'time^2/length',
 'luminosity/length^2',
 'length^2*mass/current^2*time^3',
 'tree',
 'length^2*mass/current*time^2',
 'current*time/substance',
 'length*mass/current*time^3',
 'length^3/time',
 'current*length*time',
 'length^4*mass/time^3',
 'length^2*mass/temperature*time^2',
 'current',
 '/printing_unit',
 'current^2*time^4/length^3*mass',
 'mass',
 'mass/length*time',
 'current*time/mass',
 'map',
 'maybe',
 'enum',
 'length*mass/time^2',
 'string',
 'length^1_5*mass^0_5/time',
 'number',
 'length^2/time^2',
 'mass/length^3',
 'schema',
 'edge',
 'step',
 'temperature',
 'composite',
 'particle',
 'length^0_5*mass^0_5',
 'meta',
 'time',
 '/time',
 'length^4*mass/current*time^3',
 'mass/time^3',
 'current*length^2',
 'mass/length',
 'kinetics',
 'current*time^2/length^2*mass',
 'length',
 'method',
 'length*temperature',
 'protocol',
 'length^0_5*mass^0_5/time',
 'float',
 'list',
 '/temperature*time',
 'wires',
 'printing_unit/length',
 '/length',
 'mass/length*time^2',
 'printing_unit',
 'substance',
 'length/time^2',
 'length*time/mass',
 'length^3/mass*time^2',
 'length/time',
 'integer',
 'emitter_mode',
 'length*mass/current^2*time^2',
 'current*length^2*time',
 'boundary_side',
 'bounds',
 'reaction',
 'luminosity',
 'length^2*mass/time^3',
 'mass/current*time^2',
 'mass^0_5/length^0_5*time',
 'quote',
 'length/mass',
 'length^2*mass/current*time^3',
 'array',
 'path',
 'length^1_5*mass^0_5/time^2',
 'substance/length^3',
 'substrate_role',
 '/substance',
 'process',
 'positive_float',
 'current*time',
 'boolean',
 'length^2*mass/current^2*time^2',
 'length^2/time',
 'length^2*mass/substance*temperature*time^2',
 'length^2*mass/time^2',
 'substance/time',
 'mass^0_5/length^1_5',
 'length^3*mass/current^2*time^4',
 'tuple',
 'length^2',
 'function',
 'current^2*time^3/length^2*mass',
 'mass/temperature^4*time^3',
 'length^3',
 'current^2*time^4/length^2*mass',
 'length^2*mass/time',
 'time/length',
 'mass/time^2',
 'union',
 'any']
In [4]:
# view the available processes
vi.get_processes()
Out[4]:
['DiffusionAdvection',
 'json-emitter',
 'Particles',
 'DynamicFBA',
 'composite',
 'ram-emitter',
 'console-emitter',
 'MinimalParticle']

dFBA¶

Dynamic Flux Balance Analysis (dFBA) extends traditional Flux Balance Analysis (FBA) to model the dynamic behavior of metabolic networks over time, allowing for the simulation of growth and substrate consumption in a changing environment.

In [5]:
# inspect the DynamicFBA process config
vi.process_config('DynamicFBA')
Out[5]:
{'model_file': 'string',
 'kinetic_params': 'map[tuple[float,float]]',
 'substrate_update_reactions': 'map[string]',
 'biomass_identifier': 'string',
 'bounds': 'map[bounds]'}
In [6]:
# dfba_config  = vi.process_config('DynamicFBA', dataclass=True)  # TODO get dataclass to configure
dfba_config = {
    "model_file": "textbook",
    "kinetic_params": {
        "glucose": (0.5, 1),
        "acetate": (0.5, 2)},
    "substrate_update_reactions": {
        "glucose": "EX_glc__D_e",
        "acetate": "EX_ac_e"},
    "biomass_identifier": "biomass",
    "bounds": {
        "EX_o2_e": {"lower": -2, "upper": None},
        "ATPM": {"lower": 1, "upper": 1}}}
In [7]:
# make a fresh vivarium
v1 = SpatioFluxVivarium()

# add a dFBA process
v1.add_process(
    name="dFBA",
    process_id="DynamicFBA",
    config=dfba_config)
v1.diagram(dpi='70')
Out[7]:
No description has been provided for this image
In [8]:
mol_ids = ["glucose", "acetate", "biomass"]

# add the molecular fields
for mol_id in mol_ids:
    v1.add_object(
        name=mol_id,
        path=['fields'],
        value=np.random.rand())

v1.connect_process(
    name="dFBA",
    inputs={"substrates": {
                mol_id: ['fields', mol_id]
                for mol_id in mol_ids}},
    outputs={"substrates": {
                mol_id: ['fields', mol_id]
                for mol_id in mol_ids}})

# add an emitter to save results
v1.add_emitter()
v1.diagram(dpi='70', show_values=True)
Out[8]:
No description has been provided for this image
In [9]:
v1.set_value(path=['fields', 'glucose'], value=10)
v1.set_value(path=['fields', 'biomass'], value=0.1)
field = v1.get_value(['fields'])
print(field)
{'glucose': 10, 'acetate': 0.49674878451879667, 'biomass': 0.1}
In [10]:
# save a file with the exact simulation state
v1.save(filename='dFBA_t0')
Saved file: out/dFBA_t0.json
In [11]:
v1
Out[11]:
Vivarium( 
{ 'dFBA': { 'address': 'local:DynamicFBA',
            'config': { 'biomass_identifier': 'biomass',
                        'bounds': { 'ATPM': {'lower': 1.0, 'upper': 1.0},
                                    'EX_o2_e': {'lower': -2.0, 'upper': None}},
                        'kinetic_params': { 'acetate': (0.5, 2.0),
                                            'glucose': (0.5, 1.0)},
                        'model_file': 'textbook',
                        'substrate_update_reactions': { 'acetate': 'EX_ac_e',
                                                        'glucose': 'EX_glc__D_e'}},
            'inputs': { 'substrates': { 'acetate': ['fields', 'acetate'],
                                        'biomass': ['fields', 'biomass'],
                                        'glucose': ['fields', 'glucose']}},
            'instance': <spatio_flux.processes.dfba.DynamicFBA object at 0x1277db2d0>,
            'interval': 1.0,
            'outputs': { 'substrates': { 'acetate': ['fields', 'acetate'],
                                         'biomass': ['fields', 'biomass'],
                                         'glucose': ['fields', 'glucose']}},
            'shared': None},
  'emitter': { 'address': 'local:ram-emitter',
               'config': {'emit': {'fields': 'any', 'global_time': 'any'}},
               'inputs': {'fields': ['fields'], 'global_time': ['global_time']},
               'instance': <process_bigraph.emitter.RAMEmitter object at 0x13291ddd0>,
               'outputs': None},
  'fields': {'acetate': 0.49674878451879667, 'biomass': 0.1, 'glucose': 10},
  'global_time': 0.0})
In [12]:
vx = SpatioFluxVivarium(document='out/dFBA_t0.json')
vx.run(interval=10)
vx.diagram(dpi='70', show_values=True)
Out[12]:
No description has been provided for this image
In [13]:
# run the simulation
v1.run(interval=60)
In [14]:
# view the timeseries
v1.get_timeseries(as_dataframe=True)
Out[14]:
/global_time /fields/glucose /fields/acetate /fields/biomass
0 1.0 9.904762 0.508857 0.107943
1 2.0 9.802006 0.521833 0.116515
2 3.0 9.691146 0.535728 0.125766
3 4.0 9.571550 0.550593 0.135747
4 5.0 9.442542 0.566480 0.146517
5 6.0 9.303393 0.583440 0.158137
6 7.0 9.153321 0.601519 0.170674
7 8.0 8.991487 0.620761 0.184199
8 9.0 8.816992 0.641203 0.198787
9 10.0 8.628873 0.662872 0.214522
10 11.0 8.426101 0.685783 0.231492
11 12.0 8.207576 0.709932 0.249790
12 13.0 7.972129 0.735288 0.269519
13 14.0 7.718516 0.761788 0.290787
14 15.0 7.445420 0.789322 0.313709
15 16.0 7.151453 0.817716 0.338407
16 17.0 6.835160 0.846712 0.365012
17 18.0 6.495029 0.875935 0.393661
18 19.0 6.129506 0.904846 0.424500
19 20.0 5.737022 0.932682 0.457677
20 21.0 5.316036 0.958356 0.493347
21 22.0 4.865101 0.980315 0.531667
22 23.0 4.382983 0.996310 0.572786
23 24.0 3.868848 1.003019 0.616843
24 25.0 3.322601 0.995403 0.663948
25 26.0 2.745498 0.965526 0.714152
26 27.0 2.141368 0.900225 0.767389
27 28.0 1.519242 0.776070 0.823353
28 29.0 0.899766 0.546985 0.881194
29 30.0 0.333337 0.109005 0.938665
30 31.0 0.000000 0.000000 0.976002
31 32.0 0.000000 0.000000 0.976002
32 33.0 0.000000 0.000000 0.976002
33 34.0 0.000000 0.000000 0.976002
34 35.0 0.000000 0.000000 0.976002
35 36.0 0.000000 0.000000 0.976002
36 37.0 0.000000 0.000000 0.976002
37 38.0 0.000000 0.000000 0.976002
38 39.0 0.000000 0.000000 0.976002
39 40.0 0.000000 0.000000 0.976002
40 41.0 0.000000 0.000000 0.976002
41 42.0 0.000000 0.000000 0.976002
42 43.0 0.000000 0.000000 0.976002
43 44.0 0.000000 0.000000 0.976002
44 45.0 0.000000 0.000000 0.976002
45 46.0 0.000000 0.000000 0.976002
46 47.0 0.000000 0.000000 0.976002
47 48.0 0.000000 0.000000 0.976002
48 49.0 0.000000 0.000000 0.976002
49 50.0 0.000000 0.000000 0.976002
50 51.0 0.000000 0.000000 0.976002
51 52.0 0.000000 0.000000 0.976002
52 53.0 0.000000 0.000000 0.976002
53 54.0 0.000000 0.000000 0.976002
54 55.0 0.000000 0.000000 0.976002
55 56.0 0.000000 0.000000 0.976002
56 57.0 0.000000 0.000000 0.976002
57 58.0 0.000000 0.000000 0.976002
58 59.0 0.000000 0.000000 0.976002
59 60.0 0.000000 0.000000 0.976002
In [15]:
# plot the timeseries
v1.plot_timeseries(
    subplot_size=(8, 3),
    combined_vars=[
        [  # combine the variables into a single subplot
            '/fields/glucose',
            '/fields/acetate',
            '/fields/biomass'
        ]]
)
Out[15]:
No description has been provided for this image

Spatial dFBA¶

In [16]:
mol_ids = ["glucose", "acetate", "biomass"]
rows = 3
columns = 2

# make a fresh vivarium
v2 = SpatioFluxVivarium()
for mol_id in mol_ids:
    v2.add_object(
        name=mol_id,
        path=['fields'],
        value=np.random.rand(rows, columns))

# add a dynamic FBA process at every location
for i in range(rows):
    for j in range(columns):
        dfba_name = f"dFBA[{i},{j}]"
        v2.add_process(
            name=dfba_name,
            process_id="DynamicFBA",
            config=dfba_config)
        v2.connect_process(
            name=dfba_name,
            inputs={"substrates": {
                        mol_id: ['fields', mol_id, i, j]
                        for mol_id in mol_ids}},
            outputs={"substrates": {
                        mol_id: ['fields', mol_id, i, j]
                        for mol_id in mol_ids}})

# add an emitter to save results
v2.add_emitter()
v2.diagram(dpi='70')
Out[16]:
No description has been provided for this image
In [17]:
# change some initial values
v2.merge_value(path=['fields', 'glucose', 0, 0], value=10.0)
v2.merge_value(path=['fields', 'biomass', 0, 0], value=0.1)
field = v2.get_value(['fields'])
print(field)
{'glucose': array([[10.        ,  0.39216158],
       [ 0.39736762,  0.95376673],
       [ 0.45863434,  0.77472589]]), 'acetate': array([[0.32667895, 0.14808435],
       [0.03802421, 0.77095666],
       [0.99756033, 0.22521134]]), 'biomass': array([[0.1       , 0.1719821 ],
       [0.15728937, 0.36866251],
       [0.74633324, 0.88034042]])}
In [18]:
# run a simulation
v2.run(60)
In [19]:
# get a list of all the paths so they can be plotted together in a single graph
all_paths = [
    [render_path(['fields', mol_id, i, j]) for mol_id in mol_ids]
    for i in range(rows)
    for j in range(columns)]

# plot the timeseries
v2.plot_timeseries(
    subplot_size=(8, 3),
    combined_vars=all_paths)
Out[19]:
No description has been provided for this image
In [20]:
v2.plot_snapshots()
No description has been provided for this image

Diffusion/Advection¶

This approach models the physical processes of diffusion and advection in two dimensions, providing a way to simulate how substances spread and are transported across a spatial domain, essential for understanding patterns of concentration over time and space.

In [21]:
bounds = (10.0, 10.0)
n_bins = (10, 10)
mol_ids = [
    'glucose',
    'acetate',
    'biomass'
]
diffusion_rate = 1e-1
diffusion_dt = 1e-1
advection_coeffs = {'biomass': (0, -0.1)}

# make a fresh Vivarium
v3 = SpatioFluxVivarium()

# add fields for all the molecules
for mol_id in mol_ids:
    v3.add_object(
        name=mol_id,
        path=['fields'],
        value=np.random.rand(n_bins[0], n_bins[1]))

# add a spatial diffusion advection process
v3.add_process(
    name='diffusion_advection',
    process_id='DiffusionAdvection',
    config={
       'n_bins': n_bins,
       'bounds': bounds,
       'default_diffusion_rate': diffusion_rate,
       'default_diffusion_dt': diffusion_dt,
       'advection_coeffs': advection_coeffs},
    inputs={'fields': ['fields']},
    outputs={'fields': ['fields']})

# add an emitter to save results
v3.add_emitter()
v3.diagram(dpi='70')
Out[21]:
No description has been provided for this image
In [22]:
v3.run(60)
In [23]:
v3.show_video()

COMETS¶

COMETS (Computation Of Microbial Ecosystems in Time and Space) combines dynamic FBA with spatially resolved physical processes (like diffusion and advection) to simulate the growth, metabolism, and interaction of microbial communities within a structured two-dimensional environment, capturing both biological and physical complexities.

In [24]:
bounds = (20.0, 10.0)  # Bounds of the environment
n_bins = (20, 10)
mol_ids = ['glucose', 'acetate', 'biomass']
diffusion_rate = 1e-1
diffusion_dt = 1e-1
advection_coeffs = {'biomass': (0, 0.1)}

# make a fresh vivarium
v4 = SpatioFluxVivarium()

# initialize the molecular fields
max_glc = 10
glc_field = np.random.rand(n_bins[0], n_bins[1]) * max_glc
acetate_field = np.zeros((n_bins[0], n_bins[1]))
biomass_field = np.zeros((n_bins[0], n_bins[1]))
biomass_field[0:int(1*n_bins[0]/5), int(2*n_bins[1]/5):int(3*n_bins[1]/5)] = 0.1  # place some biomass

v4.add_object(name='glucose', path=['fields'], value=glc_field)
v4.add_object(name='biomass', path=['fields'], value=biomass_field)
v4.add_object(name='acetate', path=['fields'], value=acetate_field)

# add a diffusion/advection process
v4.add_process(
    name='diffusion_advection',
    process_id='DiffusionAdvection',
    config={
       'n_bins': n_bins,
       'bounds': bounds,
       'default_diffusion_rate': diffusion_rate,
       'default_diffusion_dt': diffusion_dt,
       'advection_coeffs': advection_coeffs    },
    inputs={'fields': ['fields']},
    outputs={'fields': ['fields']})

# add a dynamic FBA process at every location
for i in range(n_bins[0]):
    for j in range(n_bins[1]):
        dfba_name = f"dFBA[{i},{j}]"
        v4.add_process(
            name=dfba_name,
            process_id="DynamicFBA",
            config=dfba_config        )
        v4.connect_process(
            name=dfba_name,
            inputs={"substrates": {
                        mol_id: ['fields', mol_id, i, j]
                        for mol_id in mol_ids}            },
            outputs={"substrates": {
                        mol_id: ['fields', mol_id, i, j]
                        for mol_id in mol_ids}})

# add an emitter to save results
v4.add_emitter()
v4.diagram(dpi='70',
    remove_nodes=[f"/dFBA[{i},{j}]" for i in range(n_bins[0]-1) for j in range(n_bins[1])]
           )
Out[24]:
No description has been provided for this image
In [25]:
v4.run(60)
In [26]:
v4.show_video()
In [27]:
v4.plot_timeseries(
    subplot_size=(8, 3),
    query=[
        '/fields/glucose/0/0',
        '/fields/acetate/0/0',
        '/fields/biomass/0/0'],
    combined_vars=[[
        '/fields/glucose/0/0',
        '/fields/acetate/0/0',
        '/fields/biomass/0/0']
    ])
Out[27]:
No description has been provided for this image

Particles¶

In [4]:
# import numpy as np
# from spatio_flux import SpatioFluxVivarium
In [5]:
v5 = SpatioFluxVivarium()
v5.process_config('Particles')
Out[5]:
{'bounds': 'tuple[float,float]',
 'n_bins': 'tuple[integer,integer]',
 'diffusion_rate': {'_type': 'float', '_default': 0.1},
 'advection_rate': {'_type': 'tuple[float,float]', '_default': (0, 0)},
 'add_probability': 'float',
 'boundary_to_add': {'_type': 'list[boundary_side]',
  '_default': ['left', 'right']},
 'boundary_to_remove': {'_type': 'list[boundary_side]',
  '_default': ['left', 'right', 'top', 'bottom']}}
In [6]:
bounds = (10.0, 20.0)  # Bounds of the environment
n_bins = (20, 40)  # Number of bins in the x and y directions

# particle movement
v5.add_process(
    name='particle_movement',
    process_id='Particles',
    config={
        'n_bins': n_bins,
        'bounds': bounds,
        'diffusion_rate': 0.1,
        'advection_rate': (0, -0.1),
        'add_probability': 0.3,
        'boundary_to_add': ['top']},
    inputs={'fields': ['fields'],
            'particles': ['particles']},
    outputs={'fields': ['fields'],
             'particles': ['particles']})

v5.initialize_process(
    path='particle_movement',
    config={'n_particles': 2})

v5.add_emitter()
v5.diagram(dpi='70')
Out[6]:
No description has been provided for this image
In [7]:
v5.save('particle_movement')
Saved file: out/particle_movement.json
In [8]:
v5.run(100)
v5_results = v5.get_results()
In [9]:
v5.plot_particles_snapshots(skip_frames=3)
Saving GIF to species_distribution_with_particles.gif
In [10]:
v5.diagram(dpi='70')
Out[10]:
No description has been provided for this image
In [11]:
v5.save(filename='v5_post_run.json', outdir='out')
Saved file: out/v5_post_run.json

Minimal particle with diffusing fields¶

In [12]:
import numpy as np
from spatio_flux import SpatioFluxVivarium
from spatio_flux.processes.particles import get_minimal_particle_composition
In [13]:
bounds = (10.0, 20.0)  # Bounds of the environment
n_bins = (10, 20)  # Number of bins in the x and y directions
mol_ids = ['glucose', 'acetate', 'biomass']
diffusion_rate = 1e-1
diffusion_dt = 1e-1
advection_coeffs = {'biomass': (0, 0.1)}

v6 = SpatioFluxVivarium()

# make two fields
v6.add_object(name='glucose',path=['fields'], value=np.ones((n_bins[0], n_bins[1])))
v6.add_object(name='acetate', path=['fields'], value=np.zeros((n_bins[0], n_bins[1])))
# diffusion advection process
v6.add_process(
    name='diffusion_advection',
    process_id='DiffusionAdvection',
    config={
       'n_bins': n_bins,
       'bounds': bounds,
       'default_diffusion_rate': diffusion_rate,
       'default_diffusion_dt': diffusion_dt,
       'advection_coeffs': advection_coeffs},
    inputs={'fields': ['fields']},
    outputs={'fields': ['fields']})
# particle movement process
v6.add_process(
    name='particle_movement',
    process_id='Particles',
    config={
        'n_bins': n_bins,
        'bounds': bounds,
        'diffusion_rate': 0.1,
        'advection_rate': (0, -0.1),
        'add_probability': 0.3,
        'boundary_to_add': ['top']},
    inputs={'fields': ['fields'],
            'particles': ['particles']},
    outputs={'fields': ['fields'],
             'particles': ['particles']})

# add a process into each particle
minimal_particle_config = {
    'reactions': {
        'grow': {
            'glucose': {
                'vmax': 0.01,
                'kcat': 0.01,
                'role': 'reactant'},
            'acetate': {
                'vmax': 0.001,
                'kcat': 0.001,
                'role': 'product'}}}}
particle_schema = get_minimal_particle_composition(v6.core, minimal_particle_config)
v6.merge_schema(path=['particles'], schema=particle_schema['particles'])

# add particles to the initial state
v6.initialize_process(
    path='particle_movement',
    config={'n_particles': 1})

v6.save(filename='v6_simulation.json', outdir='out')

v6.diagram(dpi='70')
Saved file: out/v6_simulation.json
Out[13]:
No description has been provided for this image
In [14]:
v6.run(200)
In [15]:
v6.plot_particles_snapshots(skip_frames=4)
Saving GIF to species_distribution_with_particles.gif

dFBA Particle-COMETS¶

In [40]:
from spatio_flux.processes.particles import get_dfba_particle_composition

# TODO -- method to get config from Vivarium.
dfba_config = {
    "model_file": "textbook",
    "kinetic_params": {
        "glucose": (0.5, 1),
        "acetate": (0.5, 2)},
    "substrate_update_reactions": {
        "glucose": "EX_glc__D_e",
        "acetate": "EX_ac_e"},
    "biomass_identifier": "biomass",
    "bounds": {
        "EX_o2_e": {"lower": -2, "upper": None},
        "ATPM": {"lower": 1, "upper": 1}
    }}
In [41]:
bounds = (10.0, 20.0)  # Bounds of the environment
n_bins = (2, 4)  # Number of bins in the x and y directions
mol_ids = ['glucose', 'acetate', 'biomass']
diffusion_rate = 1e-1
diffusion_dt = 1e-1
advection_coeffs = {'biomass': (0, 0.1)}

# make a fresh vivarium
v7 = SpatioFluxVivarium()
# make the fields
biomass_field = np.zeros((n_bins[0], n_bins[1]))
biomass_field[0:int(1*n_bins[0]/5), int(2*n_bins[1]/5):int(3*n_bins[1]/5)] = 0.1  # place some biomass
v7.add_object(name='glucose',path=['fields'], value=np.ones((n_bins[0], n_bins[1])))
v7.add_object(name='acetate', path=['fields'], value=np.zeros((n_bins[0], n_bins[1])))
v7.add_object(name='biomass', path=['fields'], value=biomass_field)
# diffusion advection process
v7.add_process(
    name='diffusion',
    process_id='DiffusionAdvection',
    config={
       'n_bins': n_bins,
       'bounds': bounds,
       'default_diffusion_rate': diffusion_rate,
       'default_diffusion_dt': diffusion_dt,
       'advection_coeffs': advection_coeffs},
    inputs={'fields': ['fields']},
    outputs={'fields': ['fields']})
# particle movement process
v7.add_process(
    name='particle_movement',
    process_id='Particles',
    config={
        'n_bins': n_bins,
        'bounds': bounds,
        'diffusion_rate': 0.1,
        'advection_rate': (0, -0.1),
        'add_probability': 0.3,
        'boundary_to_add': ['top']},
    inputs={'fields': ['fields'],
            'particles': ['particles']},
    outputs={'fields': ['fields'],
             'particles': ['particles']})
# add dynamic FBA process at every location
for i in range(n_bins[0]):
    for j in range(n_bins[1]):
        dfba_name = f"dFBA[{i},{j}]"
        v7.add_process(
            name=dfba_name,
            process_id="DynamicFBA",
            config=dfba_config)
        v7.connect_process(
            name=dfba_name,
            inputs={
                "substrates": {
                    mol_id: ['fields', mol_id, i, j]
                    for mol_id in mol_ids}},
            outputs={
                "substrates": {
                    mol_id: ['fields', mol_id, i, j]
                    for mol_id in mol_ids}})
# add a process into each particle
minimal_particle_config = {
    'reactions': {
        'grow': {
            'glucose': {
                'vmax': 0.01,
                'kcat': 0.01,
                'role': 'reactant'},
            'acetate': {
                'vmax': 0.001,
                'kcat': 0.001,
                'role': 'product'}}}}
# set the dfba particle process into the particle schema
particle_schema = get_dfba_particle_composition()
v7.merge_schema(path=['particles'], schema=particle_schema['particles'])
# add particles to the initial state
v7.initialize_process(
    path='particle_movement',
    config={'n_particles': 1})
# diagram
v7.diagram(dpi='70')
Out[41]:
No description has been provided for this image
In [42]:
v7.composite.composition['particles']
Out[42]:
{'_type': 'map',
 '_default': {},
 '_generate': 'generate_map',
 '_apply': 'apply_map',
 '_serialize': 'serialize_map',
 '_deserialize': 'deserialize_map',
 '_resolve': 'resolve_map',
 '_dataclass': 'dataclass_map',
 '_check': 'check_map',
 '_slice': 'slice_map',
 '_fold': 'fold_map',
 '_divide': 'divide_map',
 '_sort': 'sort_map',
 '_type_parameters': ['value'],
 '_description': 'flat mapping from keys of strings to values of any type',
 '_value': {'id': {'_type': 'string',
   '_default': '',
   '_check': 'check_string',
   '_apply': 'replace',
   '_serialize': 'serialize_string',
   '_deserialize': 'deserialize_string',
   '_dataclass': 'dataclass_string',
   '_description': '64-bit integer'},
  'position': {'_type': 'tuple',
   '_default': 'default_tuple',
   '_apply': 'apply_tuple',
   '_check': 'check_tuple',
   '_slice': 'slice_tuple',
   '_serialize': 'serialize_tuple',
   '_deserialize': 'deserialize_tuple',
   '_dataclass': 'dataclass_tuple',
   '_fold': 'fold_tuple',
   '_divide': 'divide_tuple',
   '_bind': 'bind_tuple',
   '_description': 'tuple of an ordered set of typed values',
   '_type_parameters': ['0', '1'],
   '_0': 'float',
   '_1': 'float'},
  'size': {'_type': 'float',
   '_check': 'check_float',
   '_apply': 'accumulate',
   '_serialize': 'to_string',
   '_description': '64-bit floating point precision number',
   '_default': 0.0,
   '_deserialize': 'deserialize_float',
   '_divide': 'divide_float',
   '_dataclass': 'dataclass_float',
   '_inherit': ['number']},
  'mass': {'_type': 'float',
   '_check': 'check_float',
   '_apply': 'accumulate',
   '_serialize': 'to_string',
   '_description': '64-bit floating point precision number',
   '_default': 1.0,
   '_deserialize': 'deserialize_float',
   '_divide': 'divide_float',
   '_dataclass': 'dataclass_float',
   '_inherit': ['number']},
  'local': {'_type': 'map',
   '_default': {},
   '_generate': 'generate_map',
   '_apply': 'apply_map',
   '_serialize': 'serialize_map',
   '_deserialize': 'deserialize_map',
   '_resolve': 'resolve_map',
   '_dataclass': 'dataclass_map',
   '_check': 'check_map',
   '_slice': 'slice_map',
   '_fold': 'fold_map',
   '_divide': 'divide_map',
   '_sort': 'sort_map',
   '_type_parameters': ['value'],
   '_description': 'flat mapping from keys of strings to values of any type',
   '_value': {'_type': 'float',
    '_check': 'check_float',
    '_apply': 'accumulate',
    '_serialize': 'to_string',
    '_description': '64-bit floating point precision number',
    '_default': 0.0,
    '_deserialize': 'deserialize_float',
    '_divide': 'divide_float',
    '_dataclass': 'dataclass_float',
    '_inherit': ['number']}},
  'exchange': {'_type': 'map',
   '_default': {},
   '_generate': 'generate_map',
   '_apply': 'apply_map',
   '_serialize': 'serialize_map',
   '_deserialize': 'deserialize_map',
   '_resolve': 'resolve_map',
   '_dataclass': 'dataclass_map',
   '_check': 'check_map',
   '_slice': 'slice_map',
   '_fold': 'fold_map',
   '_divide': 'divide_map',
   '_sort': 'sort_map',
   '_type_parameters': ['value'],
   '_description': 'flat mapping from keys of strings to values of any type',
   '_value': {'_type': 'float',
    '_check': 'check_float',
    '_apply': 'accumulate',
    '_serialize': 'to_string',
    '_description': '64-bit floating point precision number',
    '_default': 0.0,
    '_deserialize': 'deserialize_float',
    '_divide': 'divide_float',
    '_dataclass': 'dataclass_float',
    '_inherit': ['number']}},
  '_type': 'particle',
  'dFBA': {'_type': 'process',
   'address': {'_type': 'string', '_default': 'local:DynamicFBA'},
   'config': {'_type': 'quote',
    '_default': {'model_file': 'textbook',
     'kinetic_params': {'glucose': (0.5, 1.0), 'acetate': (0.5, 2.0)},
     'substrate_update_reactions': {'glucose': 'EX_glc__D_e',
      'acetate': 'EX_ac_e'},
     'biomass_identifier': 'biomass',
     'bounds': {'EX_o2_e': {'lower': -2.0, 'upper': None},
      'ATPM': {'lower': 1.0, 'upper': 1.0}}}},
   'inputs': {'_type': 'tree[wires]', '_default': {'substrates': ['local']}},
   'outputs': {'_type': 'tree[wires]',
    '_default': {'substrates': ['exchange']}}}}}
In [43]:
v7.composite.state.keys()
Out[43]:
dict_keys(['global_time', 'fields', 'diffusion', 'particle_movement', 'particles', 'dFBA[0,0]', 'dFBA[0,1]', 'dFBA[0,2]', 'dFBA[0,3]', 'dFBA[1,0]', 'dFBA[1,1]', 'dFBA[1,2]', 'dFBA[1,3]'])
In [44]:
# v7.run(100)
In [45]:
# v7.plot_particles_snapshots(skip_frames=4)