Skip to content

Commit a993a1b

Browse files
authored
Merge pull request #288 from StochSS/staging
Staging
2 parents df8e3e0 + d24859a commit a993a1b

10 files changed

Lines changed: 136 additions & 13 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Publish SpatialPy
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Set up Python
14+
uses: actions/setup-python@v2
15+
with:
16+
python-version: '3.7'
17+
18+
- name: Install dependencies
19+
run: |
20+
python -m pip install --upgrade pip
21+
pip install build
22+
23+
- name: Build package
24+
run: python -m build
25+
26+
- name: Publish package
27+
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
28+
with:
29+
user: __token__
30+
password: ${{ secrets.PYPI_API_TOKEN }}
32.2 KB
Loading

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,68 @@ SpatialPy provides simple object-oriented abstractions for defining a model of a
7474

7575
The `run()` method can be customized using keyword arguments to select different solvers, random seed, data return type and more. For more detailed examples on how to use SpatialPy, please see the Jupyter notebooks contained in the [examples](https://github.com/StochSS/SpatialPy/tree/main/examples) subdirectory.
7676

77+
### _Simple example to illustrate the use of SpatialPy_
78+
79+
In SpatialPy, a model is expressed as an object. Components, such as the domains, reactions, biochemical species, and characteristics such as the time span for simulation, are all defined within the model. The following Python code represents our spatial birth death model using SpatialPy's facility:
80+
81+
```python
82+
def create_birth_death(parameter_values=None):
83+
# First call the gillespy2.Model initializer.
84+
model = spatialpy.Model(name='Spatial Birth-Death')
85+
86+
# Define Domain Type IDs as constants of the Model
87+
model.HABITAT = "Habitat"
88+
89+
# Define domain points and attributes of a regional space for simulation.
90+
domain = spatialpy.Domain.create_2D_domain(
91+
xlim=(0, 1), ylim=(0, 1), nx=10, ny=10, type_id=model.HABITAT, fixed=True
92+
)
93+
model.add_domain(domain)
94+
95+
# Define variables for the biochemical species representing Rabbits.
96+
Rabbits = spatialpy.Species(name='Rabbits', diffusion_coefficient=0.1)
97+
model.add_species(Rabbits)
98+
99+
# Scatter the initial condition for Rabbits randomly over all types.
100+
init_rabbit_pop = spatialpy.ScatterInitialCondition(species='Rabbits', count=100)
101+
model.add_initial_condition(init_rabbit_pop)
102+
103+
# Define parameters for the rates of creation and destruction.
104+
kb = spatialpy.Parameter(name='k_birth', expression=10)
105+
kd = spatialpy.Parameter(name='k_death', expression=0.1)
106+
model.add_parameter([kb, kd])
107+
108+
# Define reactions channels which cause the system to change over time.
109+
# The list of reactants and products for a Reaction object are each a
110+
# Python dictionary in which the dictionary keys are Species objects
111+
# and the values are stoichiometries of the species in the reaction.
112+
birth = spatialpy.Reaction(name='birth', reactants={}, products={"Rabbits":1}, rate="k_birth")
113+
death = spatialpy.Reaction(name='death', reactants={"Rabbits":1}, products={}, rate="k_death")
114+
model.add_reaction([birth, death])
115+
116+
# Set the timespan of the simulation.
117+
tspan = spatialpy.TimeSpan.linspace(t=10, num_points=11, timestep_size=1)
118+
model.timespan(tspan)
119+
return model
120+
```
121+
122+
Given the model creation function above, the model can be simulated by first instantiating the model object, and then invoking the run() method on the object. The following code will run the model once to produce a sample trajectory:
123+
124+
```python
125+
model = create_birth_death()
126+
results = model.run()
127+
```
128+
129+
The results are then stored in a class `Results` object for single trajectory or for multiple trajectories. Results can be plotted with plotly (offline) using `plot_species()` or in matplotlib using `plot_species(use_matplotlib=True)`. For additional plotting options such as plotting from a selection of species, or statistical plotting, please see the documentation.:
130+
131+
```python
132+
results.plot_species(species='Rabbits', t_val=10, use_matplotlib=True)
133+
```
134+
135+
<p align="center">
136+
<img width="500px" src="https://raw.githubusercontent.com/StochSS/SpatialPy/readme-example/.graphics/birth-death-example-plot.png">
137+
</p>
138+
77139
<!--
78140
### Docker environment (DOES NOT WORK)
79141

examples/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Examples of using SpatialPy
2+
===========================
3+
4+
The files in this directory are runnable Python examples of using [SpatialPy](https://github.com/StochSS/SpatialPy) to perform spatial deterministic/stochastic reaction-diffusion-advection simulations. In terms of biology, they are overly simplistic and do not capture the real-life complexity of the process being modeled &ndash; the aim is not biological realism but rather to illustrate basic usage of [SpatialPy](https://github.com/StochSS/SpatialPy).
5+
6+
* [Start Here](Start_Here.ipynb) &ndash; a [Jupyter Notebook](https://jupyter-notebook.readthedocs.io/en/stable/) demonstrating the use of SpatialPy on a simple spatial Birth Death model.
7+
8+
* [3D Cylinder Demo](3D_Cylinder_Demo.ipynb) &ndash; a [Jupyter Notebook](https://jupyter-notebook.readthedocs.io/en/stable/) demonstrating reaction diffusion simulations using SpatialPy.
9+
10+
* [Gravity](Gravity.ipynb) and [Weir](Weir.ipynb) &ndash; are [Jupyter Notebook](https://jupyter-notebook.readthedocs.io/en/stable/) demonstrating fluid dynamics simulations using SpatialPy.
11+
12+
Full documentation for SpatialPy can be found at https://stochss.github.io/SpatialPy/index.html

spatialpy/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
# @website https://github.com/StochSS/SpatialPy
2222
# =============================================================================
2323

24-
__version__ = '1.0.3'
24+
__version__ = '1.0.4'
2525
__title__ = 'SpatialPy'
2626
__description__ = 'Python Interface for Spatial Stochastic Biochemical Simulations'
2727
__url__ = 'https://spatialpy.github.io/SpatialPy/'

spatialpy/core/boundarycondition.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def __init__(self, xmin=None, xmax=None, ymin=None, ymax=None, zmin=None, zmax=N
8787
if type_id is not None and not isinstance(type_id, (int, str)):
8888
raise BoundaryConditionError("Type-ID must be of type int.")
8989
elif type_id is not None:
90+
if "UnAssigned" in type_id:
91+
raise BoundaryConditionError("'UnAssigned' is not a valid type_id")
9092
type_id = f"type_{type_id}"
9193
if target is None or not (isinstance(target, (str, Species)) or
9294
type(target).__name__ == 'Species' or property in ('nu', 'rho', 'v')):

spatialpy/core/domain.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ def __init__(self, numpoints, xlim, ylim, zlim, rho0=1.0, c0=10, P0=None, gravit
6868

6969
self.vol = numpy.zeros((numpoints), dtype=float)
7070
self.mass = numpy.zeros((numpoints), dtype=float)
71-
self.type_id = numpy.array([None] * numpoints, dtype=object)
71+
self.type_id = numpy.array(["type_UnAssigned"] * numpoints, dtype=object)
7272
self.nu = numpy.zeros((numpoints), dtype=float)
7373
self.c = numpy.zeros((numpoints), dtype=float)
7474
self.rho = numpy.zeros((numpoints), dtype=float)
7575
self.fixed = numpy.zeros((numpoints), dtype=bool)
7676
self.listOfTypeIDs = []
77-
self.typeNdxMapping = OrderedDict()
77+
self.typeNdxMapping = OrderedDict({"type_UnAssigned": 0})
7878
self.typeNameMapping = None
7979

8080
self.rho0 = rho0
@@ -136,7 +136,7 @@ def compile_prep(self):
136136
137137
:raises DomainError: If a type_id is not set or rho=0 for a particle.
138138
"""
139-
if self.type_id.tolist().count(None) > 0:
139+
if self.type_id.tolist().count("type_UnAssigned") > 0:
140140
raise DomainError(f"Particles must be assigned a type_id.")
141141
if numpy.count_nonzero(self.rho) < len(self.rho):
142142
raise DomainError(f"Rho must be a positive value.")
@@ -184,7 +184,10 @@ def add_point(self, point, vol=0, mass=0, type_id=1, nu=0, fixed=False, rho=None
184184
if (char in string.punctuation and char != "_") or char == " ":
185185
raise DomainError(f"Type_id cannot contain {char}")
186186
if type_id not in self.typeNdxMapping:
187-
self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1
187+
if "UnAssigned" in type_id:
188+
self.typeNdxMapping[type_id] = 0
189+
else:
190+
self.typeNdxMapping[type_id] = len(self.typeNdxMapping)
188191

189192
if rho is None:
190193
rho = mass / vol
@@ -240,7 +243,10 @@ def set_properties(self, geometry_ivar, type_id, vol=None, mass=None, nu=None, r
240243
if (char in string.punctuation and char != "_") or char == " ":
241244
raise DomainError(f"Type_id cannot contain '{char}'")
242245
if type_id not in self.typeNdxMapping:
243-
self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1
246+
if "UnAssigned" in type_id:
247+
self.typeNdxMapping[type_id] = 0
248+
else:
249+
self.typeNdxMapping[type_id] = len(self.typeNdxMapping)
244250
# apply the type to all points, set type for any points that match
245251
count = 0
246252
on_boundary = self.find_boundary_points()
@@ -571,6 +577,9 @@ def plot_types(self, width=None, height=None, colormap=None, size=None, title=No
571577
:returns: Plotly figure of domain types if, use_matplotlib=False and return_plotly_figure=True
572578
:rtype: None or dict
573579
'''
580+
if len(self.vertices) == 0:
581+
raise DomainError("The domain does not contain particles.")
582+
574583
from spatialpy.core.result import _plotly_iterate # pylint: disable=import-outside-toplevel
575584

576585
if not use_matplotlib:
@@ -807,7 +816,10 @@ def read_stochss_subdomain_file(self, filename, type_ids=None):
807816
if (char in string.punctuation and char != "_") or char == " ":
808817
raise DomainError(f"Type_id cannot contain {char}")
809818
if type_id not in self.typeNdxMapping:
810-
self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1
819+
if "UnAssigned" in type_id:
820+
self.typeNdxMapping[type_id] = 0
821+
else:
822+
self.typeNdxMapping[type_id] = len(self.typeNdxMapping)
811823

812824
self.type_id[int(ndx)] = type_id
813825

@@ -834,13 +846,15 @@ def read_stochss_domain(cls, filename):
834846
obj = Domain(0, tuple(domain['x_lim']), tuple(domain['y_lim']), tuple(domain['z_lim']),
835847
rho0=domain['rho_0'], c0=domain['c_0'], P0=domain['p_0'], gravity=domain['gravity'])
836848

837-
for particle in domain['particles']:
849+
for i, particle in enumerate(domain['particles']):
838850
try:
839851
type_id = list(filter(
840852
lambda d_type, t_ndx=particle['type']: d_type['typeID'] == t_ndx, domain['types']
841853
))[0]['name']
842854
except IndexError:
843855
type_id = particle['type']
856+
if type_id == "Un-Assigned" or type_id == 0:
857+
type_id = "UnAssigned"
844858
# StochSS backward compatability check for rho
845859
rho = None if "rho" not in particle.keys() else particle['rho']
846860
# StochSS backward compatability check for c
@@ -906,7 +920,7 @@ def create_3D_domain(cls, xlim, ylim, zlim, nx, ny, nz, type_id=1, mass=1.0,
906920
x_list = numpy.linspace(xlim[0], xlim[1], nx)
907921
y_list = numpy.linspace(ylim[0], ylim[1], ny)
908922
z_list = numpy.linspace(zlim[0], zlim[1], nz)
909-
totalvolume = (xlim[1] - xlim[0]) * (ylim[1] - ylim[0]) * (zlim[1] - zlim[0])
923+
totalvolume = abs(xlim[1] - xlim[0]) * abs(ylim[1] - ylim[0]) * abs(zlim[1] - zlim[0])
910924
vol = totalvolume / numberparticles
911925
if vol < 0:
912926
raise DomainError("Paritcles cannot have 0 volume")
@@ -964,7 +978,7 @@ def create_2D_domain(cls, xlim, ylim, nx, ny, type_id=1, mass=1.0,
964978
# Vertices
965979
x_list = numpy.linspace(xlim[0], xlim[1], nx)
966980
y_list = numpy.linspace(ylim[0], ylim[1], ny)
967-
totalvolume = (xlim[1] - xlim[0]) * (ylim[1] - ylim[0])
981+
totalvolume = abs(xlim[1] - xlim[0]) * abs(ylim[1] - ylim[0])
968982
vol = totalvolume / numberparticles
969983
if vol < 0:
970984
raise DomainError("Paritcles cannot have 0 volume")

spatialpy/core/reaction.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,8 @@ def validate(self, coverage="build", reactants=None, products=None, propensity_f
623623
raise ReactionError("Type ids in restrict_to must be of type int or str.")
624624
if type_id == "":
625625
raise ReactionError("Type ids in restrict_to can't be an empty string.")
626+
if isinstance(type_id, str) and "UnAssigned" in type_id:
627+
raise ReactionError("'UnAssigned' is not a valid type_id.")
626628
if coverage in ("all", "initialized"):
627629
if not isinstance(type_id, str):
628630
raise ReactionError("Type ids in restrict_to must be of type str.")

spatialpy/solvers/solver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,9 @@ def __get_param_defs(self):
312312
def __get_particle_inits(self, num_chem_species):
313313
init_particles = ""
314314
if self.model.domain.type_id is None:
315-
self.model.domain.type_id = ["type 1"] * self.model.domain.get_num_voxels()
315+
self.model.domain.type_id = ["type_1"] * self.model.domain.get_num_voxels()
316316
for i, type_id in enumerate(self.model.domain.type_id):
317-
if type_id is None:
317+
if "UnAssigned" in type_id:
318318
errmsg = "Not all particles have been defined in a type. Mass and other properties must be defined"
319319
raise SimulationError(errmsg)
320320
x = self.model.domain.coordinates()[i, 0]

spatialpy/stochss/stochss_export.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,13 @@ def __build_element(stoich_species):
173173
def __get_particles(domain):
174174
s_particles = []
175175
for i, point in enumerate(domain.vertices):
176+
type_id = domain.typeNdxMapping[domain.type_id[i]]
176177
s_particle = {"fixed":bool(domain.fixed[i]),
177178
"mass":domain.mass[i],
178179
"nu":domain.nu[i],
179180
"particle_id":i,
180181
"point":list(point),
181-
"type":int(domain.type_id[i]),
182+
"type":type_id,
182183
"volume":domain.vol[i]}
183184

184185
s_particles.append(s_particle)

0 commit comments

Comments
 (0)