Skip to content

Commit f0c4a18

Browse files
authored
Merge pull request #48 from virtualcell/support-velocity
Add advection velocity to species mapping + 'user defined' parameter role
2 parents 98ed536 + d99708b commit f0c4a18

10 files changed

Lines changed: 4821 additions & 20 deletions

pyvcell/vcml/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ def add_species(self, name: str, compartment: str | Compartment) -> Species:
162162
self.species.append(species)
163163
return species
164164

165-
def add_model_parameter(self, name: str, value: float | str) -> ModelParameter:
166-
model_parameter = ModelParameter(name=name, value=value, role="model_parameter", unit="")
165+
def add_model_parameter(self, name: str, value: float | str, role: str = "user defined") -> ModelParameter:
166+
model_parameter = ModelParameter(name=name, value=value, role=role, unit="")
167167
self.model_parameters.append(model_parameter)
168168
return model_parameter
169169

pyvcell/vcml/models_app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class SpeciesMapping(VcmlNode):
5656
init_conc: float | str | None = None
5757
init_count: float | str | None = None
5858
diff_coef: float | str | None = None
59+
velocity_x: float | str | None = None
60+
velocity_y: float | str | None = None
61+
velocity_z: float | str | None = None
5962
boundary_values: list[float | str | None] = Field(default_factory=list)
6063

6164
@property
@@ -67,6 +70,9 @@ def expressions(self) -> list[str]:
6770
exps.append(self.init_count)
6871
if isinstance(self.diff_coef, str):
6972
exps.append(self.diff_coef)
73+
for velocity in (self.velocity_x, self.velocity_y, self.velocity_z):
74+
if isinstance(velocity, str):
75+
exps.append(velocity)
7076
if self.boundary_values:
7177
for value in self.boundary_values:
7278
if isinstance(value, str):

pyvcell/vcml/vcml_reader.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ def print_biomodel(cls, xml_string: str) -> None:
7070
class XMLVisitor:
7171
def visit(self, element: _Element, node: vc.VcmlNode) -> None:
7272
method_name = "visit_" + strip_namespace(element.tag)
73-
method = getattr(self, method_name, self.generic_visit)
73+
method = getattr(self, method_name, self.generic_visit_children)
7474
method(element=element, node=node)
7575

76-
def generic_visit(self, element: _Element, node: vc.VcmlNode) -> None:
76+
def generic_visit_children(self, element: _Element, node: vc.VcmlNode) -> None:
7777
for child in element:
7878
# Best-effort parse: an unmodeled or malformed physiology subtree should
7979
# cost us that subtree, not abort the whole document (and in particular not
@@ -93,7 +93,7 @@ def __init__(self, document: vc.VCMLDocument) -> None:
9393
def visit_BioModel(self, element: _Element, node: vc.VCMLDocument) -> None:
9494
name = element.get("Name", default="unnamed")
9595
node.biomodel = vc.Biomodel(name=name, version=self._parse_version(element))
96-
self.generic_visit(element, node.biomodel)
96+
self.generic_visit_children(element, node.biomodel)
9797

9898
def _parse_version(self, element: _Element) -> vc.Version | None:
9999
"""Extract a Version child element, if present."""
@@ -118,21 +118,21 @@ def _parse_version(self, element: _Element) -> vc.Version | None:
118118
def visit_Model(self, element: _Element, node: vc.Biomodel) -> None:
119119
name: str = element.get("Name", default="unnamed")
120120
node.model = vc.Model(name=name)
121-
self.generic_visit(element, node.model)
121+
self.generic_visit_children(element, node.model)
122122

123123
def visit_SimpleReaction(self, element: _Element, node: vc.Model) -> None:
124124
name: str = element.get("Name", default="unnamed")
125125
compartment_name: str = element.get("Structure", default="unknown")
126126
reaction = vc.Reaction(name=name, is_flux=False, compartment_name=compartment_name)
127127
node.reactions.append(reaction)
128-
self.generic_visit(element, reaction)
128+
self.generic_visit_children(element, reaction)
129129

130130
def visit_FluxStep(self, element: _Element, node: vc.Model) -> None:
131131
name: str = element.get("Name", default="unnamed")
132132
compartment_name: str = element.get("Structure", default="unknown")
133133
reaction = vc.Reaction(name=name, is_flux=True, compartment_name=compartment_name)
134134
node.reactions.append(reaction)
135-
self.generic_visit(element, reaction)
135+
self.generic_visit_children(element, reaction)
136136

137137
def visit_Reactant(self, element: _Element, node: vc.Reaction) -> None:
138138
compound_ref: str = element.get("LocalizedCompoundRef", default="unknown")
@@ -141,7 +141,7 @@ def visit_Reactant(self, element: _Element, node: vc.Reaction) -> None:
141141
name=compound_ref, stoichiometry=stoichiometry, species_ref_type=vc.SpeciesRefType.reactant
142142
)
143143
node.reactants.append(reaction)
144-
self.generic_visit(element, reaction)
144+
self.generic_visit_children(element, reaction)
145145

146146
def visit_Product(self, element: _Element, node: vc.Reaction) -> None:
147147
compound_ref: str = element.get("LocalizedCompoundRef", default="unknown")
@@ -150,7 +150,7 @@ def visit_Product(self, element: _Element, node: vc.Reaction) -> None:
150150
name=compound_ref, stoichiometry=stoichiometry, species_ref_type=vc.SpeciesRefType.product
151151
)
152152
node.products.append(reaction)
153-
self.generic_visit(element, reaction)
153+
self.generic_visit_children(element, reaction)
154154

155155
def visit_Kinetics(self, element: _Element, node: vc.VcmlNode) -> None:
156156
# Only attach kinetics to an actual reaction. A <Kinetics> reached with a
@@ -160,7 +160,7 @@ def visit_Kinetics(self, element: _Element, node: vc.VcmlNode) -> None:
160160
kinetics_type: str = element.get("KineticsType", default="GeneralKinetics")
161161
kinetics = vc.Kinetics(kinetics_type=kinetics_type)
162162
node.kinetics = kinetics
163-
self.generic_visit(element, kinetics)
163+
self.generic_visit_children(element, kinetics)
164164

165165
def visit_Feature(self, element: _Element, node: vc.Model) -> None:
166166
name = element.get("Name", default="unnamed")
@@ -209,15 +209,15 @@ def visit_Parameter(self, element: _Element, node: vc.VcmlNode) -> None:
209209
# A <Parameter> in a context the data model doesn't represent (rate rules,
210210
# structure/species-context mappings, electrical params, …) — skip it.
211211
return
212-
self.generic_visit(element, parameter)
212+
self.generic_visit_children(element, parameter)
213213

214214
def visit_SimulationSpec(self, element: _Element, node: vc.Biomodel) -> None:
215215
name: str = element.get("Name", default="unnamed")
216216
stochastic: bool = element.get("Stochastic", default="false").lower() == "true"
217217
default_geometry = vcg.Geometry(name="default", dim=3)
218218
application = vc.Application(name=name, stochastic=stochastic, geometry=default_geometry)
219219
node.applications.append(application)
220-
self.generic_visit(element, application)
220+
self.generic_visit_children(element, application)
221221

222222
def visit_Simulation(self, element: _Element, node: vc.Application) -> None:
223223
name: str = element.get("Name", default="unnamed")
@@ -468,7 +468,7 @@ def visit_Geometry(self, element: _Element, node: vc.Application) -> None:
468468
dim = int(element.get("Dimension", default="0"))
469469
geometry = vcg.Geometry(name=name, dim=dim)
470470
node.geometry = geometry
471-
self.generic_visit(element, geometry)
471+
self.generic_visit_children(element, geometry)
472472

473473
def visit_Extent(self, element: _Element, node: vcg.Geometry) -> None:
474474
X = float(element.get("X", default="1"))
@@ -529,7 +529,7 @@ def visit_SubVolume(self, element: _Element, node: vcg.Geometry) -> None:
529529
name=name, handle=handle, subvolume_type=subvolume_type, image_pixel_value=image_pixel_value
530530
)
531531
node.subvolumes.append(subvolume)
532-
self.generic_visit(element, subvolume)
532+
self.generic_visit_children(element, subvolume)
533533

534534
def visit_AnalyticExpression(self, element: _Element, node: vcg.SubVolume) -> None:
535535
expr: str | None = element.text
@@ -554,7 +554,7 @@ def visit_FeatureMapping(self, element: _Element, node: vc.Application) -> None:
554554
size_exp=size_exp,
555555
)
556556
node.compartment_mappings.append(mapping)
557-
self.generic_visit(element, mapping)
557+
self.generic_visit_children(element, mapping)
558558

559559
def visit_MembraneMapping(self, element: _Element, node: vc.Application) -> None:
560560
compartment_name: str = element.get("Membrane", default="unknown")
@@ -568,7 +568,7 @@ def visit_MembraneMapping(self, element: _Element, node: vc.Application) -> None
568568
size_exp=size,
569569
)
570570
node.compartment_mappings.append(mapping)
571-
self.generic_visit(element, mapping)
571+
self.generic_visit_children(element, mapping)
572572

573573
def visit_BoundariesTypes(self, element: _Element, node: vc.CompartmentMapping) -> None:
574574
switch = {"Flux": vc.BoundaryType.flux, "Value": vc.BoundaryType.value}
@@ -584,7 +584,7 @@ def visit_LocalizedCompoundSpec(self, element: _Element, node: vc.Application) -
584584
species_name: str = element.get("LocalizedCompoundRef", default="unnamed")
585585
species_mapping = vc.SpeciesMapping(species_name=species_name)
586586
node.species_mappings.append(species_mapping)
587-
self.generic_visit(element, species_mapping)
587+
self.generic_visit_children(element, species_mapping)
588588

589589
def visit_InitialConcentration(self, element: _Element, node: vc.SpeciesMapping) -> None:
590590
text: str = element.text or "0"
@@ -620,12 +620,19 @@ def visit_Diffusion(self, element: _Element, node: vc.SpeciesMapping) -> None:
620620
value: str | float = float_or_formula(text)
621621
node.diff_coef = value
622622

623+
def visit_Velocity(self, element: _Element, node: vc.SpeciesMapping) -> None:
624+
parent = element.getparent()
625+
if parent is None or strip_namespace(parent.tag) != "LocalizedCompoundSpec":
626+
return
627+
values: list[float | str] = [float_or_formula(element.get(dim, default="0.0")) for dim in ["X", "Y", "Z"]]
628+
node.velocity_x, node.velocity_y, node.velocity_z = tuple(values)
629+
623630

624631
class PrintVisitor(XMLVisitor):
625632
def visit_root(self, element: _Element, node: vc.VcmlNode) -> None:
626633
print(f"Visiting root: {element.tag}")
627-
self.generic_visit(element, node)
634+
self.generic_visit_children(element, node)
628635

629636
def visit_child(self, element: _Element, node: vc.VcmlNode) -> None:
630637
print(f"Visiting child: {element.tag}")
631-
self.generic_visit(element, node)
638+
self.generic_visit_children(element, node)

pyvcell/vcml/vcml_writer.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,21 @@ def write_species_mapping(self, mapping: SpeciesMapping, parent: _Element) -> No
381381
parent.append(boundaries_element)
382382
elif boundary_value_count > 0:
383383
raise ValueError(f"SpeciesMapping {mapping.species_name} has {boundary_value_count} boundary values")
384+
if mapping.velocity_x is not None or mapping.velocity_y is not None or mapping.velocity_z is not None:
385+
velocity_element = Element("Velocity")
386+
if mapping.velocity_x is not None:
387+
str_val_x = str(mapping.velocity_x)
388+
if str_val_x != "0.0":
389+
velocity_element.set("X", str_val_x)
390+
if mapping.velocity_y is not None:
391+
str_val_y = str(mapping.velocity_y)
392+
if str_val_y != "0.0":
393+
velocity_element.set("Y", str_val_y)
394+
if mapping.velocity_z is not None:
395+
str_val_z = str(mapping.velocity_z)
396+
if str_val_z != "0.0":
397+
velocity_element.set("Z", str_val_z)
398+
parent.append(velocity_element)
384399

385400
@staticmethod
386401
def _append_text_element(parent: _Element, tag: str, text: str | None) -> None:

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
sbml_spatial_bunny_3d_path,
1616
sbml_spatial_model_1d_path,
1717
sbml_spatial_model_3d_path,
18+
vcml_sasco_model_path,
19+
vcml_sasco_model_with_velocity_path,
1820
)
1921
from tests.fixtures.vcell_model_fixtures import ( # noqa: F401
2022
vcml_field_data_demo_arrays,

0 commit comments

Comments
 (0)