From 3d85fd17883ad8bece3408e12492df572dea2b5f Mon Sep 17 00:00:00 2001 From: BryanRumsey <44621966+BryanRumsey@users.noreply.github.com> Date: Mon, 13 Jun 2022 09:38:06 -0400 Subject: [PATCH 01/15] Update version to v1.0.4 --- spatialpy/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatialpy/__version__.py b/spatialpy/__version__.py index 5d8bc89c..5356e963 100644 --- a/spatialpy/__version__.py +++ b/spatialpy/__version__.py @@ -21,7 +21,7 @@ # @website https://github.com/StochSS/SpatialPy # ============================================================================= -__version__ = '1.0.3' +__version__ = '1.0.4' __title__ = 'SpatialPy' __description__ = 'Python Interface for Spatial Stochastic Biochemical Simulations' __url__ = 'https://spatialpy.github.io/SpatialPy/' From 4d9dc2289f1a7696e0294adc96fbade19c4833da Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 13 Jun 2022 13:10:02 -0400 Subject: [PATCH 02/15] Added github action to auto push releases to pypi. --- .github/workflows/pypi_publish.yaml | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/pypi_publish.yaml diff --git a/.github/workflows/pypi_publish.yaml b/.github/workflows/pypi_publish.yaml new file mode 100644 index 00000000..1946e301 --- /dev/null +++ b/.github/workflows/pypi_publish.yaml @@ -0,0 +1,30 @@ +name: Publish SpatialPy + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build + + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file From beabcd4f6f769e2e1e6db7fd785ac3a6ba6d4d8d Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Jun 2022 10:47:41 -0400 Subject: [PATCH 03/15] Fixed type_id assignments when creating stochss domain particles. --- spatialpy/stochss/stochss_export.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spatialpy/stochss/stochss_export.py b/spatialpy/stochss/stochss_export.py index 7fe2c545..869ce2dc 100644 --- a/spatialpy/stochss/stochss_export.py +++ b/spatialpy/stochss/stochss_export.py @@ -173,12 +173,16 @@ def __build_element(stoich_species): def __get_particles(domain): s_particles = [] for i, point in enumerate(domain.vertices): + if domain.type_id[i] is None: + type_id = 0 + else: + type_id = domain.typeNdxMapping[domain.type_id[i]] s_particle = {"fixed":bool(domain.fixed[i]), "mass":domain.mass[i], "nu":domain.nu[i], "particle_id":i, "point":list(point), - "type":int(domain.type_id[i]), + "type":type_id, "volume":domain.vol[i]} s_particles.append(s_particle) From 26dd43501f087ab34e49dcd3cf7c14c2945926c1 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Jun 2022 10:49:54 -0400 Subject: [PATCH 04/15] Fixed type_id assignments when adding points to spatialpy domains from stochss domains. --- spatialpy/core/domain.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spatialpy/core/domain.py b/spatialpy/core/domain.py index cdb033bf..c9ba7d1f 100644 --- a/spatialpy/core/domain.py +++ b/spatialpy/core/domain.py @@ -834,19 +834,25 @@ def read_stochss_domain(cls, filename): obj = Domain(0, tuple(domain['x_lim']), tuple(domain['y_lim']), tuple(domain['z_lim']), rho0=domain['rho_0'], c0=domain['c_0'], P0=domain['p_0'], gravity=domain['gravity']) - for particle in domain['particles']: + for i, particle in enumerate(domain['particles']): try: type_id = list(filter( lambda d_type, t_ndx=particle['type']: d_type['typeID'] == t_ndx, domain['types'] ))[0]['name'] except IndexError: type_id = particle['type'] + if type_id == "Un-Assigned" or type_id == 0: + type_id = "UnAssigned" # StochSS backward compatability check for rho rho = None if "rho" not in particle.keys() else particle['rho'] # StochSS backward compatability check for c c = 0 if "c" not in particle.keys() else particle['c'] obj.add_point(particle['point'], vol=particle['volume'], mass=particle['mass'], type_id=type_id, nu=particle['nu'], fixed=particle['fixed'], rho=rho, c=c) + if "UnAssigned" in obj.type_id[-1]: + obj.type_id[-1] = None + if None not in obj.typeNdxMapping: + obj.typeNdxMapping[None] = 0 return obj except KeyError as err: From 0ee8367a331960a2322cbca136add0ec5d72fed0 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Jun 2022 12:08:25 -0400 Subject: [PATCH 05/15] Added checks for UnAssigned types. --- spatialpy/core/boundarycondition.py | 2 ++ spatialpy/core/reaction.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spatialpy/core/boundarycondition.py b/spatialpy/core/boundarycondition.py index 32f25ae7..8c833c2a 100644 --- a/spatialpy/core/boundarycondition.py +++ b/spatialpy/core/boundarycondition.py @@ -87,6 +87,8 @@ def __init__(self, xmin=None, xmax=None, ymin=None, ymax=None, zmin=None, zmax=N if type_id is not None and not isinstance(type_id, (int, str)): raise BoundaryConditionError("Type-ID must be of type int.") elif type_id is not None: + if "UnAssigned" in type_id: + raise BoundaryConditionError("'UnAssigned' is not a valid type_id") type_id = f"type_{type_id}" if target is None or not (isinstance(target, (str, Species)) or type(target).__name__ == 'Species' or property in ('nu', 'rho', 'v')): diff --git a/spatialpy/core/reaction.py b/spatialpy/core/reaction.py index d0404734..098e23db 100644 --- a/spatialpy/core/reaction.py +++ b/spatialpy/core/reaction.py @@ -623,6 +623,8 @@ def validate(self, coverage="build", reactants=None, products=None, propensity_f raise ReactionError("Type ids in restrict_to must be of type int or str.") if type_id == "": raise ReactionError("Type ids in restrict_to can't be an empty string.") + if "UnAssigned" in type_id: + raise ReactionError("'UnAssigned' is not a valid type_id.") if coverage in ("all", "initialized"): if not isinstance(type_id, str): raise ReactionError("Type ids in restrict_to must be of type str.") From 6e0acac1f9cd92361274ea93eeafbac4538fa5f3 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Jun 2022 12:10:00 -0400 Subject: [PATCH 06/15] Transitioned from using 'None' as the default/un-accepted type id to 'UnAssigned'. --- spatialpy/core/domain.py | 23 ++++++++++++++--------- spatialpy/solvers/solver.py | 4 ++-- spatialpy/stochss/stochss_export.py | 5 +---- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/spatialpy/core/domain.py b/spatialpy/core/domain.py index c9ba7d1f..9d616a76 100644 --- a/spatialpy/core/domain.py +++ b/spatialpy/core/domain.py @@ -68,7 +68,7 @@ def __init__(self, numpoints, xlim, ylim, zlim, rho0=1.0, c0=10, P0=None, gravit self.vol = numpy.zeros((numpoints), dtype=float) self.mass = numpy.zeros((numpoints), dtype=float) - self.type_id = numpy.array([None] * numpoints, dtype=object) + self.type_id = numpy.array(["type_UnAssigned"] * numpoints, dtype=object) self.nu = numpy.zeros((numpoints), dtype=float) self.c = numpy.zeros((numpoints), dtype=float) self.rho = numpy.zeros((numpoints), dtype=float) @@ -136,7 +136,7 @@ def compile_prep(self): :raises DomainError: If a type_id is not set or rho=0 for a particle. """ - if self.type_id.tolist().count(None) > 0: + if self.type_id.tolist().count("type_UnAssigned") > 0: raise DomainError(f"Particles must be assigned a type_id.") if numpy.count_nonzero(self.rho) < len(self.rho): 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 if (char in string.punctuation and char != "_") or char == " ": raise DomainError(f"Type_id cannot contain {char}") if type_id not in self.typeNdxMapping: - self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 + if "UnAssigned" in type_id: + self.typeNdxMapping[type_id] = 0 + else: + self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 if rho is None: rho = mass / vol @@ -240,7 +243,10 @@ def set_properties(self, geometry_ivar, type_id, vol=None, mass=None, nu=None, r if (char in string.punctuation and char != "_") or char == " ": raise DomainError(f"Type_id cannot contain '{char}'") if type_id not in self.typeNdxMapping: - self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 + if "UnAssigned" in type_id: + self.typeNdxMapping[type_id] = 0 + else: + self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 # apply the type to all points, set type for any points that match count = 0 on_boundary = self.find_boundary_points() @@ -807,7 +813,10 @@ def read_stochss_subdomain_file(self, filename, type_ids=None): if (char in string.punctuation and char != "_") or char == " ": raise DomainError(f"Type_id cannot contain {char}") if type_id not in self.typeNdxMapping: - self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 + if "UnAssigned" in type_id: + self.typeNdxMapping[type_id] = 0 + else: + self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 self.type_id[int(ndx)] = type_id @@ -849,10 +858,6 @@ def read_stochss_domain(cls, filename): c = 0 if "c" not in particle.keys() else particle['c'] obj.add_point(particle['point'], vol=particle['volume'], mass=particle['mass'], type_id=type_id, nu=particle['nu'], fixed=particle['fixed'], rho=rho, c=c) - if "UnAssigned" in obj.type_id[-1]: - obj.type_id[-1] = None - if None not in obj.typeNdxMapping: - obj.typeNdxMapping[None] = 0 return obj except KeyError as err: diff --git a/spatialpy/solvers/solver.py b/spatialpy/solvers/solver.py index 5d46bfe9..c2da3a26 100644 --- a/spatialpy/solvers/solver.py +++ b/spatialpy/solvers/solver.py @@ -312,9 +312,9 @@ def __get_param_defs(self): def __get_particle_inits(self, num_chem_species): init_particles = "" if self.model.domain.type_id is None: - self.model.domain.type_id = ["type 1"] * self.model.domain.get_num_voxels() + self.model.domain.type_id = ["type_1"] * self.model.domain.get_num_voxels() for i, type_id in enumerate(self.model.domain.type_id): - if type_id is None: + if "UnAssigned" in type_id: errmsg = "Not all particles have been defined in a type. Mass and other properties must be defined" raise SimulationError(errmsg) x = self.model.domain.coordinates()[i, 0] diff --git a/spatialpy/stochss/stochss_export.py b/spatialpy/stochss/stochss_export.py index 869ce2dc..5d7d586d 100644 --- a/spatialpy/stochss/stochss_export.py +++ b/spatialpy/stochss/stochss_export.py @@ -173,10 +173,7 @@ def __build_element(stoich_species): def __get_particles(domain): s_particles = [] for i, point in enumerate(domain.vertices): - if domain.type_id[i] is None: - type_id = 0 - else: - type_id = domain.typeNdxMapping[domain.type_id[i]] + type_id = domain.typeNdxMapping[domain.type_id[i]] s_particle = {"fixed":bool(domain.fixed[i]), "mass":domain.mass[i], "nu":domain.nu[i], From 96f39d6f301348838c29e044975e8fb22675fede Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Jun 2022 12:15:17 -0400 Subject: [PATCH 07/15] Fixed broken tests. --- spatialpy/core/reaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatialpy/core/reaction.py b/spatialpy/core/reaction.py index 098e23db..cad800c9 100644 --- a/spatialpy/core/reaction.py +++ b/spatialpy/core/reaction.py @@ -623,7 +623,7 @@ def validate(self, coverage="build", reactants=None, products=None, propensity_f raise ReactionError("Type ids in restrict_to must be of type int or str.") if type_id == "": raise ReactionError("Type ids in restrict_to can't be an empty string.") - if "UnAssigned" in type_id: + if isinstance(type_id, str) and "UnAssigned" in type_id: raise ReactionError("'UnAssigned' is not a valid type_id.") if coverage in ("all", "initialized"): if not isinstance(type_id, str): From 3d0a35ee12c27387ece7f2ba96ee2ff05bab738d Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Sun, 10 Jul 2022 10:26:24 -0400 Subject: [PATCH 08/15] Added un-assigned type to the default typeNDXMapping. --- spatialpy/core/domain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spatialpy/core/domain.py b/spatialpy/core/domain.py index 9d616a76..e2a2f49d 100644 --- a/spatialpy/core/domain.py +++ b/spatialpy/core/domain.py @@ -74,7 +74,7 @@ def __init__(self, numpoints, xlim, ylim, zlim, rho0=1.0, c0=10, P0=None, gravit self.rho = numpy.zeros((numpoints), dtype=float) self.fixed = numpy.zeros((numpoints), dtype=bool) self.listOfTypeIDs = [] - self.typeNdxMapping = OrderedDict() + self.typeNdxMapping = OrderedDict({"type_UnAssigned": 0}) self.typeNameMapping = None self.rho0 = rho0 @@ -187,7 +187,7 @@ def add_point(self, point, vol=0, mass=0, type_id=1, nu=0, fixed=False, rho=None if "UnAssigned" in type_id: self.typeNdxMapping[type_id] = 0 else: - self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 + self.typeNdxMapping[type_id] = len(self.typeNdxMapping) if rho is None: rho = mass / vol @@ -246,7 +246,7 @@ def set_properties(self, geometry_ivar, type_id, vol=None, mass=None, nu=None, r if "UnAssigned" in type_id: self.typeNdxMapping[type_id] = 0 else: - self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 + self.typeNdxMapping[type_id] = len(self.typeNdxMapping) # apply the type to all points, set type for any points that match count = 0 on_boundary = self.find_boundary_points() @@ -816,7 +816,7 @@ def read_stochss_subdomain_file(self, filename, type_ids=None): if "UnAssigned" in type_id: self.typeNdxMapping[type_id] = 0 else: - self.typeNdxMapping[type_id] = len(self.typeNdxMapping) + 1 + self.typeNdxMapping[type_id] = len(self.typeNdxMapping) self.type_id[int(ndx)] = type_id From 801bcf2942df04d7bed94b2e7fb148dd7b569ec6 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 11 Jul 2022 11:23:39 -0400 Subject: [PATCH 09/15] Add check to properly raise error when attempting to plot and empty domain. --- spatialpy/core/domain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spatialpy/core/domain.py b/spatialpy/core/domain.py index cdb033bf..f7529929 100644 --- a/spatialpy/core/domain.py +++ b/spatialpy/core/domain.py @@ -571,6 +571,9 @@ def plot_types(self, width=None, height=None, colormap=None, size=None, title=No :returns: Plotly figure of domain types if, use_matplotlib=False and return_plotly_figure=True :rtype: None or dict ''' + if len(self.vertices) == 0: + raise DomainError("The domain does not contain particles.") + from spatialpy.core.result import _plotly_iterate # pylint: disable=import-outside-toplevel if not use_matplotlib: From 25c11e9aa82f2bec60af9d6ec8b4e92ccfe081a6 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 11 Jul 2022 12:39:34 -0400 Subject: [PATCH 10/15] Fixed total volume calculation in 2D and 3D domain creation. --- spatialpy/core/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spatialpy/core/domain.py b/spatialpy/core/domain.py index f7529929..7efb50a5 100644 --- a/spatialpy/core/domain.py +++ b/spatialpy/core/domain.py @@ -909,7 +909,7 @@ def create_3D_domain(cls, xlim, ylim, zlim, nx, ny, nz, type_id=1, mass=1.0, x_list = numpy.linspace(xlim[0], xlim[1], nx) y_list = numpy.linspace(ylim[0], ylim[1], ny) z_list = numpy.linspace(zlim[0], zlim[1], nz) - totalvolume = (xlim[1] - xlim[0]) * (ylim[1] - ylim[0]) * (zlim[1] - zlim[0]) + totalvolume = abs(xlim[1] - xlim[0]) * abs(ylim[1] - ylim[0]) * abs(zlim[1] - zlim[0]) vol = totalvolume / numberparticles if vol < 0: raise DomainError("Paritcles cannot have 0 volume") @@ -967,7 +967,7 @@ def create_2D_domain(cls, xlim, ylim, nx, ny, type_id=1, mass=1.0, # Vertices x_list = numpy.linspace(xlim[0], xlim[1], nx) y_list = numpy.linspace(ylim[0], ylim[1], ny) - totalvolume = (xlim[1] - xlim[0]) * (ylim[1] - ylim[0]) + totalvolume = abs(xlim[1] - xlim[0]) * abs(ylim[1] - ylim[0]) vol = totalvolume / numberparticles if vol < 0: raise DomainError("Paritcles cannot have 0 volume") From 7b0dbdbc6d98aaba76d7d1f3525991bedbb54df4 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 11 Jul 2022 14:01:59 -0400 Subject: [PATCH 11/15] Added birth death example image to graphic. --- .graphics/birth-death-example-plot.png | Bin 0 -> 28369 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .graphics/birth-death-example-plot.png diff --git a/.graphics/birth-death-example-plot.png b/.graphics/birth-death-example-plot.png new file mode 100644 index 0000000000000000000000000000000000000000..031b7d94fc151596a2733c4b11a8e581c9248713 GIT binary patch literal 28369 zcmZs@1z1#V+qR9wO-YxM0@B?L3P^_x(k0z6vGGVeE27?efXuNp&61=}z^z+1Xw5eZ5HS!(#p8Hc-}# z^qo&+&zG_7J;5y2A3qO1+W1m8eW|?9aWQCewGD`?f|#%Ab8~5ti)Fp59i9zeWbrGQ z$*0SUQx*?kc?sfSXj z99d65$F1%YM=~K=x3@sfu%1gV5syk)mCC~q2dR;mp9Rw?ro7IPt*51>E#MOms^xgy z!kfmiND0&Z#a85D ze;sOp=c~R)L2)_vmN-rwIm=kkh8)8qOP(-mp0I(Ozy~?^PjM%b((6A;G5%aUrYM;j zM6v@bT3tN6*s%zi6x7vEhJu6hc6_Xf#pvOUn15}0QmsHv$b66w2}vJA884mOe@_x37mCTPXJ z5Tln)vKDYB8b71NS8LDhv);Loy@gC2&v>HSb0_Mcqz}??mI#TRkun z7u_jc3tG5tC}&)2(&%8Cg9jmaVTXx>>E_DWzL8=1Q^vFq%fq0)jtF?jht7!_xXXtlQ`ZBAo?W$u$Fh~XwFVJ|RL;1}`=hFekjM~?I{W2B zCZe^;4{&>gOqQ<56#Bd;Q$SUFivUlr}?+tFA#Jhn1I| zf@B3a3^bSbIYB%^n9IjVk-e50N&MW2utg`N_QRiB_Yxew_NbR^sgWA0(SB6Sujg!< zeXmQ;lob#6vba@N%8Xq{e7|3K93y@HGOHKT@g;V6t(Y3k$PugO(C(Tk6IP3>oK6HM z9h2U^cIv)YZD3TZZ7lkA(yPIo0s^~W6;-5uMWap$p2gR{ofZCi(Hk}cvlz-jPdu7!9IU_h@e#Ab7Mp8~d?v!_c6vbqrr)1B zJS$p>ps-J*7#t3>%G{f$UI2COS2R-R-95J`zqDm-H(}0;WAY38`nE0IsB33oZ_)F4 zxrXiR#_^R_ofmS`CHbz+v7#GKytKOaXq?S6uV>+mnSpe?rQ~ zuf~BcE!7MbTalKOthNl&G=MY%@(%2;c zGLT4LnlkfzBiAb2YyjnllgB*hqF9yhr9u>9VsZ|x-mWgCgf>}=l;||8K>1xnDFTbF zW^EGNW)W_@8Q{HIP3?c~wwu*fx%ejAIZRRrhp|Y;#?O_X%#?u^R-OGNv)^5Bu{OHI zea2o@T2}H)OCt>l2}#jU74;T`lTTztdc{VPpFWd1tUSF1>25#D+MMaG2Wx{GqWMV$T*Vor9t0tzV9HxEv>Lr?QwX}|mZe4cX90*mWSCeiJ z$REs|Et-Z9LFWcL0?Wi5H~iJayYUj)7&SDH-q@=obbw5PLkDkmDBB*q{A3VKlNsW~vgfKl*$ zUrE;<;|OTFaF+O-kfc~29S*Yame(ns%6P@kGQ)&1oSM&UT|N*o3ucO#I+_s5=pHLd zNKM%J9Zu95+*!5+>1xI@FtokSBIKb@G+N8#+T^5$ynmXS(#6ozP$Htbt3rE?k~9&s zSC5b>(uLLW`WD@j*n+~-ni6kX0UNKY)HzwjQG3p~CNcNGE@W5eRpB8B-$%%W*9kdt zlx1jC!t=U&6{^F1Pv6n;LsX#Irx&BK>)8EXH`9}7qDN;^G`$z|LG{F(^Y(m{Wm4jl z==9<%i(BgbIAjL@AX6Yb6`gL2(0pu>dj9;1gr!WV_#JzNy|TXNQU3S2v(79e__W6< zbAD~TvFxF(5BQ6XYsJrp_GS(qJZe`W$joEksno4t9{6%|%z~+x^_Is8ogf z59k-(ZER{pM;~;oS2+PQaeIy~yvJ8adCCO+XgFDeJqv<~Su7qh7|~yJ)Pg$ZEMFJS z^I0qQt3!W@EWeF9D9v>*B%bT*`bobI>nM#9bs;PAWBnzgM*&^z?}}*Pm7FoKblAd} zZ!*^`HI^cl=ER{|jl5i;En6!9)+9K1;Z2)}MLd%%VbOW!gK*vca&r~**_XON%}_j( z^ucA30C?DS98?_lD3fO&wu$n`lGTIKbItQlLZQCjt*el(dP}wkNCi=vCgh4bBNnXI zXHktCjt4#`0nY%}kGx_j5OP^el3pbt%+!uwJ}uXC(DG518F)<=!VK6p@js>)KTK7x z?(5&?3XUiT=sE9TWGJjSx2ZaSgF9-MwlgJ_vfYz$KT}S(F`j za)YU#*e25WvbMvjmfc3ipw2c+K9=RpK2biHQg;`aAFEKn(w_BIZLMq$i1ndA63x%tlSq=AZt3O-8_OVhIc*v}6Z zLI<%OswPqoN@}IP7~1U*_U(W<&;7gpmvPtDS!L(_t&?;-0Zdo2KeGb85E)`M@DXb? zF^GcvVox_)=m;TTS}n!ql5#~hz9o5rOA5BjZelws*OZs}vRMuTB{&>aLNDf)p0@hv z)*z3l{dtlGBlZvYQ_;M_tHzj8_`|aVW(8U(14>)~C2nj@Sn?XBsfwQ4ITv4K2!Ww9 zmXm3tqoZz2Kf?5?L@q&_H@rbrrMnB?DN*!lIp{yf&gB*lP2d4MNwLnv;8 zBkn~<$Oi20$B*AN;&Pv8&r9XeD7)bxaOUu$A3tHQdNZt-S@dxz{K3}OAo!r~I zU!xB|1F8YDNV>T=rgd0eoVg6cFp?6yP__%#|2^Is*7`98lo4q;Emr>*Zz}FxC?Wa?6`EZa zG3=U{tx5L$!cEr2b?}6x$iVdcvyl>csmWOl&hPNBgUPlFdU|?`V=v`i964U{Z;&Nt zS|zg2YTGGV$4u4>z@wQ>CW+VV)k}u5q(002C1OSN3Nvp8)1IQ20?esdUz+Szw?(8Q zQ~~m70mUW>3R37GZ=Pjs0DpeldCTdg z`$))?`eTP8q_Zg!bhk|i^2~ql){D5Obe|AyrPQ+2vx?BdUZ{YEIHjjUP(fGD?^$r(BhTP+U zny_>(7`q(_os2i2;NTGr;r6S+w+fLhCQgXdwuv=yd;cE`UVx-Wn&s|n45Q@hz zL-Va$Eo}#9F(``%x}Rr^V^YJaajSqU+NC1D&J_Rt2tPA3j;sRH$IHsKJG&9i<|3(< zCFO{*2x+zJgVfVPM^EP`np`PsdU^&Y&$r+j|poKla7m@pl{Q{JCf|xfnS# zlPneUXmyFOYmXv?;IMqX)0eG#^LVZB6lAcLUsn}@ym^=K=GCw56B2-#p==(Zg0M*7 z<)e(97iH%lC{Fdu#5wYS$`J3MDOLU#ylr5Emj7}rS(8hslf{%u-z7!P zx%s5(=K8(wwor-bgLUzd#~n)@5`4_U9Hvth)s5tfhkj`v<-G6_MZIYsOT zc_q#-lOqypM5}7!@JTUzeaP0XCd#MboF$Iyngrj#qBlpxQGPlTmmU^a3?tsGNG&o7 zOv|5fB*3#u-W0-Bwqn>NI7$?p@1gweIk#&vR-P(zdagsTtg-YMxPc5262I-luue1k zTe2x8ru4;`n}v9%gXy=5w$MS967~E@5_}hT;81(_;>9x;ZG|LlLIRRE!-52rnlNLf zmn&*TAOMOir|GX;qOXxhRC{1iV^iS?mY{LnMg$j?T!3k3UUD`Xq_Qy=8?BU_O7neMY}t@-fiGt*gdP=xQ(3NMi`1meroh#;038_4fhMXr7@)HPfTVv>j{JboAfVPNGzR~iV%W9AJ+*#?D4)3FG^HWT{x{&f5GH( zidpbXZd8&10;Oif$G6P{M%{n0(IzYI#iV}cIJjl8Zmj?1-#TTYCpb>rX!2<|PbxDN zjmjanyh1T{Zow{}w;|*&@;P?2Pm`xik$#Or*~;WUP31q$?C23K;$4$fmgT2>v$M=hC*ypLaa2Ee9WG~FH7*JyXb~(anUfi%09$hvdRvOh|8#Q4Y zC1S5iQ|eQ^^1u3kQmKzpseBoFe7eFlI zz=MxLh5_rDFX9D)v>zjb>3Ar)6sS`KZ<;1=o;vUBSvh>Exn#ndHEpKtA5qs&r{(O` z^jDH|U9@EJAf7>T2rnDJiC;c0lA!#C>lF_o$#Drai4zxFep9`P{Ha6z(EWubz5f+& z)N@_zqHkZn)Z@LPqYKQ=&d#4AE^xFKUFkYn>3qpyBsu2Rnw?DrhzH34H$5pSt#1MS z`<bjrColZ40W*T}$cX!` z;2t-gxGus#0%&pZ=#KW=d%5dUv%>~hR>>otcpB0j$qVYd=S^GQ-05bbu#l-W8Xg{j z?8}~ze59jWo-_<=h3$a3{qA)~9+5=Q>Tw~z4H}{RR0Po(UkRLbwU#k!POUFJJn)y| zcAl-qj|?eYHU;4_N>-(1#!u zE0HhFEVhiuzy;273VzOLXVzj5d0EERCuLWFsGJxhg>8NJs>^S&`T{^H4& zZI0!QSH4t!JZzqD^za$u>YjMUd+hpv&z!^EsFihHLrsmx+d`COv5Vrd$-k;kJFRZH zBLZR|%9H&Y2V=db=z3udT@&UYCe!PGjBvH$O%yv(u+P{uas`z_N4)BfS#PfJ5Fv-j z0X33jOR?6tB!X0>*E&wOpnp z#=?E0tWTY%%i!;Zpo#pw&11ROqCfbD6%YFd6pr^OhIG>m7k0&$d&on`*y3S`X>FAj zrYWjAo9hR+1}0;ZUl8GA`pwrXtOm(j@4qCD)Km~`CSBcU5wK0DDRgO=$(nEV0xC$Wwq7ppA__4m=`OAdTT@dbv{~p4ZuD4K zEiq^f#p@{=8CNyf0m*sk$9El;js214_c|tQ%aLJyVoUcRK)JRTyA*g(cL2^vc<}|; z?a^Uj)n>^(ZuGwAxL7-MA9(X}!!)7%WFrBc9fFFI;i{m%>$W@fIiJP7C7n;z=lFAw zWyB0@S(!0-QbACmPTGm_#I%jN*FyqPa9qWspAQjr+rvpLB~-`%TXti_LkpL4u+!0z ztZ+u%=Fr-O(-GJrP?KL@L%Duq)|-wII1d#~tB6^x?fW6|5^57?3QD6iL($$n5)YqG zJnr_E777Wg2**9|ZDVYy1C>AQ5+Z+G8mfxwF{x$(3hlJCXDz_?w`6D%u!;PK%PXNZ z;)yS!W4@oR(B=~WsuW?FS-J*=FX5fLeJ#pAo4Zp939$V6!6LUk1 zb3=8W6Av^01-3L#{*yp+W7Lt11BEIs8wI-$rnS;lok}77I48AlXGBsT$Vz?bzeesx z;%1MHZ>sf@WE;%we7jp@!S!4-pET5XA&|YBm}Z|_Y_~u0#b2?0GhOKO_y;<5>-P?0 zb&NK=OxFmUFALTJGGq3Z`}%2@ooh<=g!a(*28HAkUjR}++`PypGij+ubGmClxq&k+ zW~z_KNN72J7D> zV?#f(YQom480Oh6NiBztpf!O`SUp?8?!U1HJ0|xIP1Klka~t?CR~=O1C&&pym*<>@ zyJv1ZDj4QHy95-6bI!542%u@kmO2DL z0U&6r>l*C?AW>sBh6WZ;bO@62X1Lv%u_kfEyiQENJtQ8Qmq&Bk8`*WHaCN@4<*Hws zblOf&4(ZHdcesOlxCtbA5mh{_T-se3M2&26Tz*@Y^%0Dz)FiG{&fWSL|I*9*!bESC zJ6n_G?mp6BeHFbYCT8!IKyP~&QP?`gBZNAH=SIx^v@))=!Fb1nyxD@b;iV-%K>%vQ zjIig}MA+cS=G0qi1kdy3c%AaKl27*zv-mgpc}EM$=BI4|`)pkcz^%z2Tli*RUstd^ zYTTKq%mjIi?PX7<0ZjUm{GO>Gd_9YV?RB{_R+?)-G{Vi%72hVO`&HrFw<9-!R9sBy zJqEY5u?1K?D^(-ZTmkNo!R01cYvY(HbR#-buf0}7iHdrm>mGu!IbR)k@;lR>mPHMg zWlJ`~FJepupdy#MqKb;j*hi}z!~Ye=MjCf{Frk}27f(9Ovu`=wy?5{$e;lAViT-i4 zL+#3*^iy0rb#(}4+9c6mT$|`gRc>hpaII-y+<$A6A^;bo&RI^T5HP9#(4jQ@3*+<; zg6DzNi0w|3gTnsC#IdK9zW%ZQX`_F+tw~TB{g10IxzEit!2joXNLspA^09_vR5O}I zi@XVkuAHD@ojeK!T9%fU{{_$M89&p_fb9(?Qg9Ie1>MS*l8W_aBtR^p*D_$n((EaV z5iY~E;u_J>F^P^ug8bJQ0d|i7u`6`g>T=Eo{VQFNr!Y@K@qEm3(u>9O@6&ngoCuQq z6FUG?N;*_a*J7^(cInL|6ND$Re2&0Am|61_V|~rSbc&!h3@kiMNg!P2zC=c3_?I z%tiz>bXq*=l-u~#De$+`!jH;ae(~}mViqJMB*n+FGKbTl+g&ZFgV_Cz8F-K&_C-=+ zp?ul~Vy?+bYkG656CT{ubF$98W<$!v%7dT{fH72cx{LD+94j99@jNkqr#v%{Hk>k_ zIoTNZXlZV>!;^=^$D}9pDVh_q3pU-$mI*lmGHVP$@#P=Bx+AV0BIz1%lJ2e{*OeZ> zzEst|r{ZZ%KLabg65IcWw6AcNlJN0;y_#70;rx%DTi4OCe~fjdcUk6Dq0qzn=;s<& z1cub6|H-d>S);RRxStzV(b??URISuT3>oZW^L0d0rNtUukx@EM&pWIJKPv60iNOJd zKl%;U%J3Ll=nZ=Dh!}o<tWQW!d-=KyvK%d2?vHhQrR86?WUjqse^ZJ(GD> zVm>hxgN8!7SPWC~Tc@idc`$kjU-9*RnmgPKUuD@7@d0qaM{#YFg1I}V2$8teOSnkx zovp~DR;BuXG*aQqlf(~dO~=JQ4^uocl`pLz^G74N-A{0fR50`1myDO3bA%GwjK(fRd zHV9K5%XY=;X_ugLvv2>JZ-Tyjd^JwLwD?{_a<8}FnTi+0@1+8Kp0rXWg<2vB9ysjM|-?Iu+y zfAp__{#*m)utC(QHq^^%y}a?;d)qqn*9Hd*F^G@IC?m84?Wqxxu~y7;)W*NQJH7r;f8mP=jC#!@!S)@So9o3_25Uv zq^}b+c4_G}N4^(>nDFi6H)-Gh{%#8V^Tsu-@49?8;&ZQy8PSPJlNwVAWU} z$PU;|y|s~>i(9?8)~Yg0e3G5AA1m15C?z?;lt=0%cBrXfE?p&?1XYiThh-$=T1^?q z&R88<)Q@fd67hG!lC}!LE6&^R5m#Z(EojJQEEZwzP^yp2G?KNu zfz&)?ZLOf}m&(dE>cL>DMg6FQR-rr@%jT0*h$5hK`!OtKXS^xN*+r(jl337145(XI zNN(ESv#VZ(h4oIl4#~O}zTLg^pIt&aKI-%pZ_diPqDeqrgr*6+O?**pKK##eN<%DI z(1t}{wS*9@`(o<6(@X0iC~Ch3zTWxc)x{8)`vyuoxM2h;Z>P^Tb%bc!4D_cGyQE#z z*Mg~7b^1m1i?N^YFN8jbaCRIJ4T+Gdq6hj5L)1e9LRnwnscG#S;_z0fni;n*S~bNs z*Qh!*NbORD=twF4Ftv3*U`Fw1ig`IE4~uu9!#a%2nOWMZq-h^6(W`WvsHp@3C5Q|; zp3X$THMlj+J?>#Lk2QftuU0t8D$$DQDrG$E#J`@gB_qN{@@;a({KDzET7_gRfX@WQ zSNh$7BjL;d%gsgflQ#=rkI^!&oGW=vH{&2^^Zey_VCFqIg4Ka|d&T*9{8}T%3=(Bj z9SgjyoQ{59YmD^2^j6FP?n1B^5SGrdDhF*_;0Clu7$DGK&dvvMG{?-s*uXbW97!#2zi7VySRi6Zp#n9zPCzy_wL<5x)5~x`WUZvZ(dL7aNxr~SbL4=BL$Wv z3}Ps_w5IDuhu;$UNlWh~Dw{UwO*0t3=9$G7u5I~GrhSK0Zy%|6(9v;K*&kx!NxeSE zdS45tQ%BxWZWTs0ffXpR#CE+gescjTCx1O5ikKL)uv)8mFQKx2Uhyz($VtlDZGN#I zvl5SOLp4`(iISJ9;k5FODE4;^q#5rf90bPbjg}y9qAv309lke(mRv>sS_3~z;|(bM zaza1*VIB}Z-=MUA-xJkS_i^$#SIE*Bj$@59hH59LEQov=MlQH81zRn#MvigycAZfo zGzmu-)w288JhDb2L>W=YgtY8NGAyi^NR_pAVo4ZQqI6>oWjS-<(CM_uj`v#QivEe2 zO3J7C)I)-Rklfk!*xJ9nYh{QNydxf9LEF6fOZi34a{wY#0rX%HK+ABgw_&;@5M}2Ta}PVen-{;$c?j8Ci&QzeRN91cJNm&pg*BJy4$rNRHx&@z;%hr;r~^oJ2eJ8MGx=kuXRiJPvbe9MaBF4XmMd`~ zy+9j)bNn>yk8PY67eBc4+P@y}Z#D2x&QjFX9?*KjUT|b@7s#rp-~d`4kbM2Jx@vT> zFH~!>QKD51WTK4L`{EVnx+5u+eSCaE`L(pP6p}du04jd3u0C1geK6k;myl2excm9G z0RaKI1qIdMm5zK{fd26D@hePvWAbON$S{$C8(w~HuK)g=vSRbXol>e);6~55CW6eF zvDLxS$P&XyM6XdLcl;c%h!a_O-4MAx8@lsde?qtO5@S3OsBzK+pbu;u6g)Dr8U?yFX{S*@GTxBL*r zp2(Q=G`Y8Cu8Axzs}8r*6;$=(w^s~b92$*My~SLSZ=vB z9OCh*&0nehT@&=AjR^fcGoh5Gp4VnSlCb+jd|JH7n<@T4#^Q$aF23*Ii=|&3SrboX zazs!JrP6u-8505-hZ1i$@_4Efbimty7l8@b-Kxy{p! zhxln`_1w6#ih`rdUTAdeI5q8MMWt796di2j>Ur*0gi5m;-`k>!!$9sIrqpzCtP37RKY+AF&_jfy&hn0`Z zr~tqL3Y;jc8xZdB2Mv~<;%3=7scyGzjDX=LI3eG%u@-duT1@j%eBdA}>XYE_UZJ;> zyuv$hSRWpr7w*iV))cN1eejh3l}n;Hi4&zC(I957v99_gPM&t=-5HPd185Qz{)pBEEf-kq~BT8oUkd+GA<;sMEnVS5(i-PWoI^k1Q64P!R1z|XHDv5i`= zS1lOS}Pw_;8sF;)+y9^D=n-I@;$b6!th?sH=B)*Q#Z=a*` z8CutGUQ&q17+HHfSm_<5YQk@B{04%;GMW1H>ei|In8ookIbXMNjqk3G;dJ>w($`^= z5s0L-I;xI32P1*IYYge@>Hx`-K|z%6#@Oo2UJ(Euk6h?J-}@56{B)kzJN)|&`jZxW z33cZPT}Gy0IrZs#AfwRb!^xK<^R_NaC6tWdx@=_qQnL-^N{IdXtMM+P^&yAD*gdiT&E$cIkTqji= zUx9jAviD5kkgtMayX;7P1!Uu8J1%)L+5w4?32P@cezA}vjV22`0N>tLZHu7`&Z>}%rZizJC;eK9T>ZKx`X4wwgbGPF61geyJ zvn|bk%+IWDn6(1hEY+^wtsuRgF(PTe4IwX-op-wakmia+`EjA0Sl5*#=d?OW>)Rm_ zSQ_Q!0=0N6M)Pa`X%u`oSnwi~5_-KfXQKEUInhN}DCE+{RGsHcwA!z5#$9i<`gYnu zU%%-SwHi+v7k_5MnpCt2)TP-N@sHhaR}t9My}1q(;;X#->2os5S#S9RQ4jWpI2u$1 zL|>R$wIkzVPB)cIMg=;Jo}O}=^}ck;L$B>#sI(>N?`Jk?&eU(B*!#8ye|F{@O2bJL zbD$5YU2`g!(?xS{;Jhuk1R8`|n|!8O08PP$gcWfviM5ZU@145@10&;3l?gg~J%r7N zR|l3EwgLoCuWq(vy}X^L9&7`!6$H)sV~4hHOqFg-FPAgsnjYo_N?psy-~XIIKx-0C z0OM1U>QVIMnQ&!qLK=l5I*Y)Azc_TcoD~v#i23MK9llfn`&iBHJ=QcU@Z78q{|Y=K zX@M_}=x@*qV%ilmQ&1BCeht43hEw4kFn z9NItzQU`y61>I>_|I?IH8<2(6$I0K`R5P&lG+qZAqOS^mro8`*P zW{0h4oTIVz4=G&NeFU-2dXl)`P9#~B?>4+!b!(9Ps)s1Lp4JT0XHZF;n1}IzrB2OR2Do>YP$|y{)un&!knR2!HJ=|pgIw+$ND@7Jd7mhu>!LsvE zesWx0v(o}Kn9XdjDPwGSTLqi_4waI=iu?$fNN>-%zS_M&dV2e{`en*mPoCaKJH3cX zE3{6FkQp~LMAhjmy}V7jT|9l#y;}XcMSAbu%!dg#ysywah98g0mzsO5m6@{|tH^;Q zsqxrbpS$yd8K`RJ_Bs#?fJemZM!;%mKWOPeL45rb_&MB6JRS(m+{G=xnJ9O7JbyHq zkxo5wR|71(VIsSYrqhSuohd=qS7E;N8Q$CI89oP2N@*q3*im~7BHfYXKCy#ym@ca* zv!F)U=fj69o;q8x&-t=N#S6`6H)s!ceai;Bjry|TV z*(0(XPx@EFru^h-XnZKpTmP)WFPi=Gljr-)1-0 z#dM}l7KRrOoo7lLeD_cB$#f-JHrCA!>DATuR;~LVW%J6FE;aW@ksv9klyU*sss7kq z;z3EYpJk{0ZxnXa% zA5V=a%gIDyKqHt=^Tu&e^TVW7v%c?lJ!7Nw7+gjstC8_HW4wwO?_H`A2r}(L8=Ivb zC*8am%Uf%Sm6;&r8tchKXZN`7D-#4)cPzu{Q^hAMJ*nTz90LD1c9RFfKR=zw-OUy{ z5kXL*OR5t{fv=qv!K z%NZ>v|G{|VBc#ujRi>P{!EDQ$`v}F(p<|SKLUM-raaFG-gY5uw%T7c^sb!WIwGa?r zPU`I(`Ym0VNcj@;kpuu}<23Q{Pej++;Tp16{D%hXZCv zKq*<=0hV`3WtFw^%TI6SKJs?;tx(4|cFj2bM$``Cx6AB{_|oGXs=9lrQt-8myk{sBEYV2|lKItu4;&xnVYKWM*XU0i zl@_A3FNb9`cJ=N|A@~Mm&U8@yq?pu5e1RE=;Me@nBHl_K6{9bbZLfnNeei9OKX%Wy zt|?~k`HOkFofBffBjutF25ap4vG))&eL3+AwAsA4<*B;;zZPk=$H&^5@U<$Y=RllG z$OPyQDV{lBNNcVM%lDNABsmq4+i(6d{=i$RjT<;%_udv%ZuvbPMA&;$!hjXa831I0 z%4F+tfoP8=lYc!IVJQH=PRQX@vXzFGS5f!Pj(mWonij{MZ?-!{8JiOQ11V9tZNY&w*i1znrnF9 zdl3J1bZ=hEzs(ZraGX4n*#y?~UtT;`bzlId;8@Yom*7p_Kj}D(}1N~(xUyV zd)4zM5*s&dOzP2FLnxy0eOD#$Ry-fuNU>V_yT72KoB&S!i+X7k z2fLul8u<>|d6U$1$j1DS27znMfaTHLMco*&v`2kM302(5P){GxL?-)mDMu^tS!dgd zxBKtjat;fB(WO2bn)W(aWscMJ`EZ#{(RO~mV1xUj*1SW%GvJ*XRWsdfPuPRp7G+9+ zT#r!}cmsvp_dIoaZv;!h@(ioXmM^_?kWYc8bY&jn=YB6WmOhYKYyfH-qocOPyW?o< zmz?yP63R+dPWuG0^^bEzwM_<|2U!)Gh{{X8EJRAAUk9z)jc^oR3ixeO)MuEO&lzB( zuhfmiUXA3wG!cx2+scd?THDm0HHs5|(x5X_+INWzhW!_3P+rMaI@})}-BUvbu1qIw#4E3nYFpz--2lyk{{A zBB02PYrZ{bdeR_6OFvhhk!Py`iN@|0rd9v!PM%co%c#ugORbGkZ)n!i`TV5P`c5p19 z8U_$bdQwaGJkKGH<^W>VWK+xZbd`uKe&zUj}DH;)i@ zXd{4A3nc5z&?)QCDP2)1ea44w%Lv6~DE$vYD-|=#?1juBM*r*mI=JxdRIpR}6I=n1lM))W)>owLeh3pglOYo5@ zU4j39eU$##MpVBH!P*Iq2)s2h+}4-0OkWbzsfY2L zK>Rr(=Ce~j5@Lfmkoq%4zGQUD1h^%8wwdnFDr|!6b^qIU)JWngs>_gv=7oNRGW}08 zYu-GS|5YCL>PZ=)O>U)D``^yB?gC!T7;`qSF2lOlM<3wh1h7sQU#3r4$s4D}{@u(} z-vDvK4gcr#Ps`F+grw~s8+@;lhw3jYUE>l_vHoik%fQnll!mIHdEfR^@=5W@8J<46 z4ATerr(;2RfPCb?o$SbBPk>xs(ys7Q#MyHHHFN9^Mrh|GN%EdPKfwrJvXdHzcfll26UsCG43;-KuqL1!36+89kG=3(*2huva+*P0NnA@aU3`5t4)R&~W1 zAi$

`w@nh#m_xSwW+=e;QeBT%XlkPSPkj<;InWR0d*U5mA2%bwW!TcY3eDbnS(HxxQiZE52!5FQ&Y?h`v{Lo>U5=wQ9136q5O--O6i2|LB zJ$n5d4fh0l-akK#{n08%!KVlQ2Qr*oRNTKaTz9&At1qs~^MS{2%(OsR=^tN@{nXbq z5TE3o9O=kWsJ+3?IPpXMeZhFq#1*`LiFi9>iI?$F>a_ukDrwBkW zzP@DXgT(y9$=vr(a6XEirm9=XaX(0)OK*mAkBYaYx}y<8}t_)7Z2cQU)?c6?zUljf?lN1smKYc_a*8+u4_oT*=uq? z;K~nmhL-Bs+KAr2EXi~Fr9XT{?tZr&rX#4(=n)Q*>Dyro9169I>*$8vMMi=&p{LmL ze2B3uZOy^nKyOWkPW@L20=34jks3JPyOCWt5uXn{aDF}eYg_Gj_#%%1tQ^GVy4r4M zwncYzK+is8*#gu>baW!Vy)<$fYHvP>6M~rmw4jp@4tpX`Rps2s08jf&jv@h&967zV z-`rtOxg}ivJ>xo0?cG{6L1k~?3V^;<; zDD7TDr`3UjP7RxrTvnO&v;+O5$G1I1?+-6lPLZDg6r{Dk5p15o{U0A2YQ!XjrN-@@ z@MNxQT%XH_9$$?sB`(S2m1n?g9ODp)^Sb$4cA9P36T)#lyan^ zbczZH($XCw-Q6HKLO>cxk&^BlJwiZ0L~^8bjUJ892fd!_M6w<_uWgfbx5$|>@>E}2|qkO=a~37`(Om-vpJ(PW9CIquhncB1x!NHl4&BI za&3;l2(Mef5Zel!zSll!2GiLuKVCHhXuAyhAT*^6hrh=NZfR5ang~5l!KM)HR$`h2 z{Q-p=i%YX$rl70b>K-~~8P3I|NSZp8Y)FQkdN@|1T8q&(F@ z77QlHX{V#0wWj1FD$Qcj80yoD2$Ie~?C9}po2Z)Sg%6p6Pa0()4r|9+j@ny6mJUSN zF?W2#Cd@v z9i7T*PUGsWars~1HYpZOi8!K{sE3XO8cJA7!DwF3Pr)4k_7ik^3p`@Fd^!Cpz7I@l zAul+o13y)rV+(EKCiaUhQG@G^W*#3AE-yq!a`K{~v;N8?G{woRv`sTPC+l!8FrX|0oCggC}S&Hblc6C}Nd@@S&Zd@SoXDW&TBwG z-Q%3SSLiVIvuaYNMo6YY$X?s}+<2vHt9(WT&iMNA!hh3+-xae!z?Co zByU)}+DRtkaSOKba=h4Pi~VuxIlnnPfadSuNlRBe?=lhr48TB<>9lwQ+=HGo#mq z2-xLNs($uVm0ZnCi~QBJ=Gy+C466iSMPO#ErXViuv&`?Yu~}+6Vt$-*3c5Ii^^_eM z+wX;NQP^TC<#}ppmEZPn|C;&WSCJJUMt{dpKke1~z-MOf z-;+t9$*Z?*p9%9$RR;706DFc5(92`*0+PJT{0*=gZLNUt` z*}Mr;-l{yU-@dR@92`&npV;@vbZUCJll6ER!#{I&2Peh3HHv<|-!;f6OdB}wZ*64p zY$44o6y*K3Mn*>eYCx{j=Cz^G0DL?pedh4tQBy%-;Nz8nkRFNWdFlBUEEJ0F@|sNS6WQ)eb=MFMAtj3Rs;SK09gTVu&QmVudg4z z!YXp;?ch%5RiLm+na67>VY1NUV=Yim!28WLbXUe=i!RJ@kf>(NI#<0oQc!{E#}7|U zjmD_&7tx&TbfLey`^;3RN{@d1bA|-+vDk$f$E#1CG?-ZaUB+11FaLCa#w;B*6z`TK z3(4QJWhgWt)|QwZj2U{qN{Q?}*>FE7^plbejf?SPY5Bq(PHT8<>XXhOHkGRif2fsWbMoOndhR`n&$kzTp*vXAMtgc`HZL z#``cf4BF|w5&DCS0})v2C{_9xT(y4EFJ4#^^)vOmpedw_v=8XU@$nh3jW4Q43@he?G^mY$$Lfwy%EB0d^M5FJxh95Nee*}oIkQ{>+RiAFvr(yn z)<7bwYa5=GPeLdB)lEFyB6pe}6F+s_B zfWnid`J+LswLq3Y{7r$E;ecWa5Bu_m&Si;j>`Yv|Mx}`xMe=Axefz@U{-n-nNB)(; znBQOMPT-7Wd?F3^6IqH2H5ZH(z7pUj9S~i_YaD3agPr zkWU==K)bWGV{^MT@3Jgn^t^fnRY)fZ+F?+Wr98y9s+tw9k64-q1CyV7EeUu(Rv;J2 zK39w!())P3T0b7%HwVh_XMu6!Wx@&*ND1aF};h;M! z`~@sQ8JQnCUbp$?)oBX5wrvoux%rg5td;=8)?2s*({)yJIjC+!ecpR(J6M7JI6|>S z+be!Hg8Lh4>I9ul%@-iix9v@EoNz|0fL20dgP(Jf6b2_#UNje37EIYRdtW>Ejp$uC zrHN&`z%*SI!>P^Pc5!QL7Bi)b8M*t}dBY8Pu3g6sMbqc3;B%Xn`35ZT6#KzCdy#=S z)vaU~R!VNkw&M?gz>5{9k5!V70QTe$NuS9u&~`x^cDnH?q$W>f1o_b*%-6;u))T+o z)&?Q6T{f9C_3$*O1&|f7MjN4h$v@0sSa^&o-O@C9qPYsR@A}Pb&I0|<8^8Xs){fgB zGx!+`#cqO($sOPo(BbvF!$Ypo&jJk1dq3x{fL{I2Mx!_OYyfr1AT`u&X-?y6iKZP0 z{j$vK$~+!md55ba#%z@|kMRIpmMh+s{b$R`{lb@_L!F^RilM`p;b|UBopq4Q&~#oY zwWkdaDPG$Y{BWWT%Rm&`7+s&n*^HCY-|ORM5CYG9#HW&OTgFcyWw$t^WCT-s42gr%z4b*aA z{cVQ^g*0JkU)N4~{R<&CP3t|QI@?y7aZXgO0gif+00-AHO1PTd`YIuH{iz8sh8md$ zu9SAS*Xi^1ed@)@qIKaG_ae`9hxwJg>oL5?(UClI~SowbSEo5%ZXfpyXbL zQAa3%U39%9QN`8&bF}CJ#lsU`RvEK4ilK)RrDI-^i)*ZLx>pBKHHD&W)xd#IhcxLs64E* z`D)sA@ZC>v&HrZE&KIc};VM$5uRWq~@G85F@Ri}e>+AxW2EBPp`}sQw4<5OY40{c- zjO6~ZN#o_E1$inBDBfl&Za*riHzv-KUaW#CN`rz1@Ep2zj(1uLch6o9q^P!@II(^T zDll@3$x46Xt&O@SEea)15(%R4;bMT#6s{eD7ir~VvT!@^XNZyA~Jy|oNaEOoi=Ec=6Ge-U0r4TU6MgO_IcN>fI+ky zZ9{r?G7!43QO=kkFVdZ|xV=wrSH!QsR}rIJzPRn*@*FZ~TRLT~;SFS`F({M$4}Zx8 zP(e~3>}RilTY4>7E%ub2oKj56o7{Q0MFD8XxBU~y;?ZdP-$vE3CFO`~{d258@}%|2(zGs0G*90dgh zn)QyahHG{gx??jkX74JftCOnczA)+xyThq|Ck@Q(<>cgWvdD9Ba4?CAHg$^5|J8G& zHHE_4HYX|}5QxzkYRe`vf2is3fk#tYWW`hQ+~rG}i!|b`q-2=eftzdxsJ+qkRg(>Z zKWEO9+V#9VKe@q*ZjQd7@{+f~kNTViJhZ0kNt`*2P4g%65}9#(It(*jj60l$4RICI zwr-Vgt*GB^{#lBgvaDOHcl=r9l{L9ic|zb!4$Jg#;k=J|42#k=khI*g_(yJF`~3dYIBS@lOly z3@!5hMdXn^r5XqJZg$~nU7pvBS zfTDpzH{5hy{gW#vn3+UG9BoJi7!jAyx*i(8wUiqA4w-h1gA&XVQEjvH|Lr>h@v1jm1g|G) z2s2+%0TZZ#O+!{(!Bsze_rBuHf?>nXKH4A5uF~YW!$oy<0{<4$iS+10q78U(rm1Ds zZi>H?eDGbD92+KGiR|A&KA-OI@zzw=B)8E)yl0=s; zrNjZm0*5CoSLRZ$5dXrBrJc0G0V8{b;}OFwqtPNQ`Cl_H9pa?NIE+*~l|5l9$D_v( zUC`dK;1$jW)sOPS4Am;(x`V0I$BZX49ZoWp(kaq~PWD_!<_o~grNS$NVnC&S^5@?+@F)^gd)Jrs|6JT?7< z#mebEi#^=qcA55pjoN_~rJ`?nn8uimBo3w~3I#o%J9qjbJN#>^S}u@fDu^a$PEC#L zWW|ZPxa1i=Ci#ct4x}fyiDQNPNrIJ&p3Gk9w)X&gMoIUAYQS96I2SY`(a(_)1GC<=$T(Xm~ZP2}Q>0>M&qE0QK-gDUEOH?~HdtfyLMmzbl z{Jly=SXqvgWFYlcBdXTLLLuCQA}r4|R~KHaR9nop8I5fBUA}p;aXg)HS0q4HE?k(C zk2b@Z{WevMEqEgoP*>~YZShVQs*RF>uS|ox6!pLqZ`o~Eh@VcjYesPyY)(L>)hNJ^ zAF0hR8l&rd+^-|!RMYZ4+ct|N=Bx*jwH;=e5v3u1&>|{34=)xLP+pP3$Al5K@Rf0k z$!9IDJ{YAD0U@h)Z6er59lL8J)4HJQVb<2WBP{iJp+U({`$#a|tcSFsw|>0jW7a|Qqi+R9te9vW2v(CztwyL@3U(K7uxqCZqm?Qy(QW%s0RG{M zc4_pjem0!iowwJUw~_AvZOMWFWx%IKYv^@24}hAFy=bgHE*wG)5bdop8ORAJHgLvp z`m@fh*dY|BiK&3oq<@l49yK-_XU*V_TrQpJD({_iMZJrhzQdXf7#s1*{?h zCA6A+$u384FV&6Itd!^S8S=dweaUV77iEI_7B|OMa^U>CCIR#E~~j5i-7NjD=9& ztv46CqUB*Vba;84Qu-BQHm7J@osrYT4R|?GsJ$GEJV>Ot^qr;gG)()5NA`auS&@+emPYYt;E43tn7w)JT9X`mL z)w$OA9~4wf!r3^c1nemX)NR@o!Xt#C>Dm&^fV}cPJP#d@PiXlG<{Jas1RD;NAMx<9 zhM((Gj8j_H+tJ>k$?Gk5TANTeYYWb_satff<)G~jgvG@VD)M<2X0 z^wV!X6q@!@r)xgByZnM87;g5gT>A_1L+`e2OWcE#(}Sv&l@$zoR|Q%>ffeG@x|nh; zjyjTHs<&i2`|bNKp~E4n%I+KAOL_7Y#Yv1F6Oj(A4oB5;!H8amB->?QW**OInC-+n zb8rOeA!0C zwoExOd-!;M)Z`ooE~p}TFWq+X@YU8p(408GKKvjPqny{<40_bFc#=}GhEm22oqU@D zu#IHZjl**HHmGv3109~P8hq<8HhJ-z3&~Re_~N-im`B98pYu`i$stfCqWSx0XhLdl zY`5reTXN@7tA0pr#+>ZjneaX%c&@WI@~#5`RC+)2`SS|m3#A5gB@D{L4D^6ZWyXet9U#HwlbB&>^4~Mqz(QC0aMJRK30SCGV<%W zPGg-oO+LhKo?W28eKl(yit`*yD$E#Q@8%O47ZMtW6aH-0{Bctmj`gLtH|(Et-@4O> z+t{iB+s`3mI1DNQ06vKViYW`y0Y<)iNqoGTVE?rBW@wC5dKp`z-};SVGt^wW@c&F) zVw8Nfc}w)~KAzjWfG*fqzke;{lurV*;`(|In$B{C|5^{KtCyqi&-8%i zvlt*FGS_A>n;CP8BKd$l2Xf5N=cXNxWHUcy85v1pQ?;3%#l-CuYkvI$0H0<$QI%ld z;`+s3CNF!U`E`kWzUu}MA;Wajw%+!rcvUAm@ZNz1`!#NB&Cv14kfJP9)r8*nZcZwl zJT=@oIX)qw>)*y?giYO$h!C4f?_f?G*rZU9)%vy*!Zr~Y@V~VS#ocgt2&c1B?jx>x ze+uHn2c!Cb#E*UlA4ic65M`Vk9Pj@9u=i8iuE;2kxsjeD|JsjVSWo|mGX^F!@yNLT zb(8a{tj6M6s@O^+%ikqUB8C9M@DvvG|5%JN-&le}h{4Jb-_Gk+ToKNkbDP33n%4bR zidKg?A;_zI+Q1YP?R@MZ>`Rr;!wT)=EUK#vp2}cm0bfH7t^51uRH^ zPVNe3e5>+4g+^77MtS3?Y|j3{*?QCi9?nECvDG$YZPcTAp>rEsX0(X=)yQNM04D2J z&xa1y(sJb{iYNR&#e9-S$W%{iX?glT+;@aIPcSnM%@;TQeF1GJwhPJzDXjvKCH^47 zeSh|xxUp&tTc%pn~ErBNQgJmR0kj{ve2w6+U7+xPhm;7Qg#tX4A~dvu#6o^Snc zmROh(xn9qiZZAup9pxQJV0<-oa8C948h&X3s4+*!CiGevWJrVOeRXT2Sp4?+sv0VR z7A$9*)M}N3DFq~%hU$G+e~|g58&s!@b6rxa4P(oM4(Hl0f!|K&9*>5u2@S7z2=z-m z1BuS869=uWgEH3rj7nrKRIlY3nHpzn6s_l*ejS2&)NwgPorAOuF5aA)AN<5Q97Czf zflB<$kVu=}pA>tpY9dL{nm$U;<2m%XMy-yQjnCqbbPeh1!N5H~m%e1P=$G>Yn6A$u zU?$Y3wNPRsetsE!koWuyf6jZTr?e`0hxF2z@;K^m~Y ztWEc?^eDti_r@67=cCgJkI#W)iKH9ECIA^n6FKQ*L#2Jrp4q<93LMB2Z}08mlU2dI zXT^5hu)<4u2~Ts~8$$G=oB6&8+Ox>3g!iIWJR9IAg?y>P85tL>g~wwng(k5CaN+IJ z*GK7&Q|WW{`zO32wBjwtTV9xx6iyRhJ)+mr@&x|rmAl#gHoI%PEelN47oXfadbwM>MfV4N=FqN)mj@JgV0ezF4_nJA{OtJL64d^)yB3yS z({Z#H7JzVnmi9XVSd59G@zQFrDg`((OE1C`=J3^)38$9M{^P{{8+?RhvoxT%)FkqJ z&-^NUS@mc0!x>mbHFYX9G5h$o)?N&8UOgjSXbn=f^jmswu-B{+XB8CS`XBMzb>~^w z=!>i9!G|m_nxFP=h(GJCamaoNDg5>5xA;(1SM6)-tx*j;eH1}Z6ZaJOd19B&a^V}9 zM74)~xV)03Ht@#HkvRQBBKnwjmDi+(^~4>J&F-vmPqTn*J=Sk{5no>f9Y;TGUa z1glJW{=g7vp zF5+5ud(5FF=AL%_N!nA9tH3l7|1rB3Jp?!)*NXlJ@%&8EO_3G@SfX=*v?8bu*RIGH zD}e9eYpSiOA+8nXU3|dn1~$d2oYzz~Te+oB1RgJF?IKFL4;Wc6cC5N(S}8qvmIJcU;Xb4XjI?{Y3%{fK*!Q5mulM*Q zL>JAs?tv!c^rG&qN&A@d@8;C=Z^Hd|E#RVf?wYb5%J6+U4B&79YcPs(rcxI>6HPi` zrXsCf*b+YNQx{0(cZNIjHbdlSki6AI4z*AQxf($4kr~R%dM?`&PaxiRxz|gI00TC3 z-mk7}C{Q8R$N@KcHJ~PPxMX=*y>g@U}iweerN3A=bfMl5+ zBiDX5M9v=^7*W32mnO2;eT4Yo=w-^ZyNoy6VL6iLG`^tm;dH#^^FG#sh=3znlrG)p?Nqkcytx60X5)45!NjDl1>!RD}wcv;s z%DGrnj1AMxfu6;bM8(b$O^3-lm-Xk^U<#D6c3>1Nl4CO*x4@R)M}y$o!YYj4=0Jig zLr6Q3Jc2Gs2bYz{LPA`Z4-0C{JYk!q;YjS)a7qlSoj+A7D5f_abF>Gj)s^9rks7^z zIhgS@%{m2SGUbI_3=aiEO<^=px(nk5y~v>!B-2icnRSshF^5!v!$ZnOSPi)zJDxQsEcJoLJLpliGWAMXmth zc3OFlcU2sys(gFjYWW_uD;8`ztnU@8V9$V^uW!69xF)txJw|9O&s9u-yE|SOfP-2A ze}-Tt+_aEg<$CwkAScV+WPe?32b>*&HY1}!8HgWe&@pqFlcIDt2WS1BkZWxc_mHo=?Qv(mK{a|0)E#l|4@UodS-QFPsGE46q*$Mb&k;D=^B;#ur9 z^_QEAN*6e9vr(@kqs~cJxYU0s#UaH9PVvuVS849ce89ucD(5kYYBPK8X`Q?X2@%~~P`Q!Wq~-p>rG>9s^v>X%a; z0Gs5Z1SQ6u6cC!jAQ{?V$doS$`M zZ7$3q>mJ)A1=wq+vY7d^$V212eU3e3_$Nuna}t{x81@c{ANs32S=ZpUFg|I1|LeiK zt72(1{WXgB%A!J&*t_otKj~tZ*FUrczGVc));}i7`}JIV?#2wFr_pA;{sZa6%7kW* zQmT2V__73j0S(&!3Q@kP5L@rnm{v(@43x`pm#QiH9lh+~zt|VQDx`->?+i^#u}=^o z9;LDLo7+2&ye_XMY1EgNF}CF(-5u+swUoA1tm@e1xxz@yHQc}Ij`~QI-ho+y>1V*E z1P#iLj@AF12!##*nJq{0X2~>KlqL^0Dj4@D||YjIg2CIu%w20A_p(G zA(31ae%LhhUWMiBn>oxCSioa{nsKx%l-~~hm*w%1$XvC%m-M{YZ5i;7>Cm6F`F`*- zm4HG2JFVH^0}e+=0l19Wd}Y(v~K zo|%DNZzgSgelEF+ji*rX*eT}39pWo$piS2yWuQ+Tk=J~&&Lb1vbS%g-A>GjY5>?VM z->W3JRhkIgbx4%ZhPMn~^BJ?+#st?x*d=?!k@cpI{SR2r1^_geKT8R4~C5 zp8lLkMPU2xv*Oa6@}E<0a_5|X6z;rBALAr`lN_wsOJbKxVn^S5v%u)!M_vbJPv6h= zvdEpL_}axi=H zF7IWKE`bPp&UwpQwJHO!wHVFwnDyp;9Jx;;_5{~1QbY%B#!2@?@#QM=FuGx#B|di@ zKzsk%j_RBvM5ow<)<9@a5vvPLE{HG(6R|KO|30!(X5ixSoE{{{K(?N2{PF#3-6=9t zb{P6M-m7r2y3e*W%VE3G5czOjYRFUtQ6(}@xO{rz#?XN|hZEStr#wkWWie(TtHqr2 zHBMz&4ziC^^VonZXVZOcsf0CVW0@wf8e|Y3FjL4U^|TsNLOOiJ?Zx~FjhWhlyoz)< zO;+^gmfsHHs)A|OrD%7#&ItFT$!cH1&5^0cWh$uT-{eQmghV8mWcD0OnCBCNzw75kos8L|{j z8cn>-fT;G`EL!u>X5+7>`Efqx@4UC8pdMFTTjwHKD_oCzyo%s>Ike}`6rPqy8V3G9hTvXj literal 0 HcmV?d00001 From b12c440a8ddce6e3ba45d0db4157d658fb3e2441 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 11 Jul 2022 14:27:06 -0400 Subject: [PATCH 12/15] Added a simple example to the README. --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/README.md b/README.md index 2e6a4d93..a8879d94 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,68 @@ SpatialPy provides simple object-oriented abstractions for defining a model of a 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. +### _Simple example to illustrate the use of SpatialPy_ + +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: + +```python +def create_birth_death(parameter_values=None): + # First call the gillespy2.Model initializer. + model = spatialpy.Model(name='Spatial Birth-Death') + + # Define Domain Type IDs as constants of the Model + model.HABITAT = "Habitat" + + # Define domain points and attributes of a regional space for simulation. + domain = spatialpy.Domain.create_2D_domain( + xlim=(0, 1), ylim=(0, 1), nx=10, ny=10, type_id=model.HABITAT, fixed=True + ) + model.add_domain(domain) + + # Define variables for the biochemical species representing Rabbits. + Rabbits = spatialpy.Species(name='Rabbits', diffusion_coefficient=0.1) + model.add_species(Rabbits) + + # Scatter the initial condition for Rabbits randomly over all types. + init_rabbit_pop = spatialpy.ScatterInitialCondition(species='Rabbits', count=100) + model.add_initial_condition(init_rabbit_pop) + + # Define parameters for the rates of creation and destruction. + kb = spatialpy.Parameter(name='k_birth', expression=10) + kd = spatialpy.Parameter(name='k_death', expression=0.1) + model.add_parameter([kb, kd]) + + # Define reactions channels which cause the system to change over time. + # The list of reactants and products for a Reaction object are each a + # Python dictionary in which the dictionary keys are Species objects + # and the values are stoichiometries of the species in the reaction. + birth = spatialpy.Reaction(name='birth', reactants={}, products={"Rabbits":1}, rate="k_birth") + death = spatialpy.Reaction(name='death', reactants={"Rabbits":1}, products={}, rate="k_death") + model.add_reaction([birth, death]) + + # Set the timespan of the simulation. + tspan = spatialpy.TimeSpan.linspace(t=10, num_points=11, timestep_size=1) + model.timespan(tspan) + return model +``` + +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: + +```python +model = create_birth_death() +results = model.run() +``` + +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.: + +```python +results.plot_species(species='Rabbits', t_val=10, use_matplotlib=True) +``` + +

+ +

+