diff --git a/pyvcell/data_model/simulation.py b/pyvcell/data_model/simulation.py index 5a15830..a54f27e 100644 --- a/pyvcell/data_model/simulation.py +++ b/pyvcell/data_model/simulation.py @@ -7,6 +7,7 @@ from pyvcell.api.vcell_client import ApiClient, ApiResponse, Configuration, SolverResourceApi from pyvcell.data_model.result import Result from pyvcell.data_model.spatial_model import SpatialModel +from pyvcell.data_model.vcml_spatial_model import VcmlSpatialModel from pyvcell.solvers.fvsolver import solve as fvsolve @@ -40,7 +41,9 @@ def run(self, duration: float | None = None, output_time_step: float | None = No # create temp file to write sbml document to sbml_path = self.out_dir / "model.xml" self.model.export(sbml_path) - response: ApiResponse[bytearray] = solver_api.get_fv_solver_input_from_sbml_with_http_info(str(sbml_path)) + response: ApiResponse[bytearray] = solver_api.get_fv_solver_input_from_sbml_with_http_info( + str(sbml_path), duration=duration, output_time_step=output_time_step + ) sbml_path.unlink() if response.status_code != 200: raise ValueError(f"Failed to get solver input files: {response}") @@ -69,3 +72,60 @@ def run(self, duration: float | None = None, output_time_step: float | None = No def cleanup(self) -> None: shutil.rmtree(self.out_dir) + + +class VcmlSpatialSimulation: + model: VcmlSpatialModel + out_dir: Path + + def __init__(self, model: VcmlSpatialModel, out_dir: Path | None = None): + self.model = model + if out_dir is None: + self.out_dir = Path(tempfile.mkdtemp(prefix="out_dir_")) + else: + self.out_dir = out_dir + + def run(self, simulation_name: str) -> Result: + # create an unauthenticated API client + api_url: str = "https://vcell-dev.cam.uchc.edu" # vcell base url + api_client = ApiClient(Configuration(host=api_url)) + solver_api = SolverResourceApi(api_client) + + # prepare solver input files + # 1. upload the SBML model and retrieve generated solver inputs as a zip file + # 2. extract the zip archive into the output directory + # 3. remove the zip archive + # create temp file to write sbml document to + vcml_path = self.out_dir / "model.xml" + self.model.export(vcml_path) + response: ApiResponse[bytearray] = solver_api.get_fv_solver_input_from_vcml_with_http_info( + vcml_file=str(vcml_path), simulation_name=simulation_name + ) + vcml_path.unlink() + if response.status_code != 200: + raise ValueError(f"Failed to get solver input files: {response}") + zip_archive = self.out_dir / "solver_input_files.zip" + with open(zip_archive, "wb") as f: + f.write(response.data) + shutil.unpack_archive(zip_archive, self.out_dir) + zip_archive.unlink() + + # identify sim_id and job_id from the solver input files + files: list[str] = os.listdir(self.out_dir) + fv_input_file: Path | None = next((self.out_dir / file for file in files if file.endswith(".fvinput")), None) + vcg_input_file: Path | None = next((self.out_dir / file for file in files if file.endswith(".vcg")), None) + if fv_input_file is None or vcg_input_file is None: + raise ValueError(".fvinput file or .vcg file not found") + sim_id = int(fv_input_file.name.split("_")[1]) + job_id = int(fv_input_file.name.split("_")[2]) + + # run the simulation + ret_code = fvsolve(input_file=fv_input_file, vcg_file=vcg_input_file, output_dir=self.out_dir) + if ret_code != 0: + raise ValueError(f"Error in solve: {ret_code}") + + # return the result + return Result(solver_output_dir=self.out_dir, sim_id=sim_id, job_id=job_id) + + def cleanup(self) -> None: + shutil.rmtree(self.out_dir) diff --git a/pyvcell/data_model/vcml_spatial_model.py b/pyvcell/data_model/vcml_spatial_model.py new file mode 100644 index 0000000..9b442f1 --- /dev/null +++ b/pyvcell/data_model/vcml_spatial_model.py @@ -0,0 +1,64 @@ +import os +from pathlib import Path +from typing import Any, Union + +import pyvcell.vcml as vc + + +class VcmlSpatialModel: + """ + Spatial extension of `libsbml.Model`. All class methods are inherited from `libsbml.Model`: see libsbml documentation for more details. + This class is constructed with one of 3 entrypoints: either the filepath to a valid SBMLSpatial model, OR level, version, model_id, OR model_id + """ + + _bio_model: vc.Biomodel + + def __init__(self, filepath: Path) -> None: + reader: vc.VcmlReader = vc.VcmlReader() + # read filepath as string + with open(filepath, "r") as file: + self._bio_model = reader.parse_biomodel(file.read()) + + @property + def bio_model(self) -> vc.Biomodel: + return self._bio_model + + @property + def model(self) -> vc.Model: + if self._bio_model.model is None: + raise ValueError("Model is not defined in the VCML document.") + return self._bio_model.model + + def export(self, filename: Union[os.PathLike[str], str]) -> None: + document = vc.VCMLDocument(biomodel=self._bio_model) + vcml_str: str = vc.VcmlWriter().write_vcml(document=document) + with open(filename, "w") as file: + file.write(vcml_str) + + @property + def model_parameters(self) -> list[vc.ModelParameter]: + if self._bio_model.model is None: + raise ValueError("Model is not defined in the VCML document.") + return self._bio_model.model.model_parameters + + @property + def kinetics_parameters(self) -> list[vc.KineticsParameter]: + kinetics_parameters: list[vc.KineticsParameter] = [] + for reaction in self.model.reactions: + if reaction.kinetics is None: + continue + for param in reaction.kinetics.kinetics_parameters: + param.reaction_name = reaction.name + kinetics_parameters.append(param) + return kinetics_parameters + + @property + def applications(self) -> list[vc.Application]: + return self.bio_model.applications + + @property + def simulations(self) -> list[vc.Simulation]: + sims: list[vc.Simulation] = [] + for app in self.applications: + sims.extend(app.simulations) + return sims diff --git a/pyvcell/vcml/__init__.py b/pyvcell/vcml/__init__.py index 9fdbbb9..1d3cf82 100644 --- a/pyvcell/vcml/__init__.py +++ b/pyvcell/vcml/__init__.py @@ -9,6 +9,7 @@ Model, ModelParameter, Reaction, + Simulation, Species, SpeciesMapping, SpeciesReference, @@ -40,4 +41,6 @@ "SurfaceClass", "SpeciesMapping", "BoundaryType", + "Application", + "Simulation", ] diff --git a/pyvcell/vcml/models.py b/pyvcell/vcml/models.py index f348109..19025c9 100644 --- a/pyvcell/vcml/models.py +++ b/pyvcell/vcml/models.py @@ -33,7 +33,7 @@ class ModelParameter(Parameter): class KineticsParameter(Parameter): - pass + reaction_name: str class Kinetics(VcmlNode): @@ -145,6 +145,13 @@ class ReactionMapping(VcmlNode): included: bool = True +class Simulation(VcmlNode): + name: str + duration: float + output_time_step: float + mesh_size: tuple[int, int, int] + + class Application(VcmlNode): name: str stochastic: bool @@ -152,6 +159,7 @@ class Application(VcmlNode): compartment_mappings: list[CompartmentMapping] = Field(default_factory=list) species_mappings: list[SpeciesMapping] = Field(default_factory=list) reaction_mappings: list[ReactionMapping] = Field(default_factory=list) + simulations: list[Simulation] = Field(default_factory=list) class Biomodel(VcmlNode): diff --git a/pyvcell/vcml/vcml_reader.py b/pyvcell/vcml/vcml_reader.py index b61096e..07c83a7 100644 --- a/pyvcell/vcml/vcml_reader.py +++ b/pyvcell/vcml/vcml_reader.py @@ -26,13 +26,15 @@ def strip_namespace(tag: str) -> str: class VcmlReader: @staticmethod - def parse_biomodel(xml_string: str) -> vc.Biomodel | None: + def parse_biomodel(xml_string: str) -> vc.Biomodel: xml_string = xml_string.replace('', "") xml_string = xml_string.replace("", "") root = etree.fromstring(xml_string) document = vc.VCMLDocument() visitor = BiomodelVisitor(document) visitor.visit(root, document) + if visitor.document.biomodel is None: + raise ValueError("No biomodel found") return visitor.document.biomodel @classmethod @@ -135,7 +137,13 @@ def visit_Parameter(self, element: _Element, node: vc.Model | vc.Kinetics) -> No parameter = model_parameter elif strip_namespace(parent.tag) == "Kinetics": kinetics: vc.Kinetics = node # type: ignore[assignment] - kinetics_parameter = vc.KineticsParameter(name=name, value=value, role=role, unit=unit) + reaction_node = parent.getparent() + if reaction_node is None: + raise ValueError("Kinetics element has no parent") + reaction_name = reaction_node.get("Name", default="unknown") + kinetics_parameter = vc.KineticsParameter( + name=name, value=value, role=role, unit=unit, reaction_name=reaction_name + ) kinetics.kinetics_parameters.append(kinetics_parameter) parameter = kinetics_parameter else: @@ -150,6 +158,32 @@ def visit_SimulationSpec(self, element: _Element, node: vc.Biomodel) -> None: node.applications.append(application) self.generic_visit(element, application) + def visit_Simulation(self, element: _Element, node: vc.Application) -> None: + name: str = element.get("Name", default="unnamed") + duration: float | None = None + output_time_step: float | None = None + mesh_size: tuple[int, int, int] | None = None + for sim_child in element: + if strip_namespace(sim_child.tag) == "SolverTaskDescription": + solver_task_description_element = sim_child + for child in solver_task_description_element: + if strip_namespace(child.tag) == "TimeBound": + duration = float(child.get("EndTime", default="5.0")) + elif strip_namespace(child.tag) == "OutputOptions": + output_time_step = float(child.get("OutputTimeStep", default="0.1")) + elif strip_namespace(sim_child.tag) == "MeshSpecification": + mesh_specification_element = sim_child + for mesh_child in mesh_specification_element: + if strip_namespace(mesh_child.tag) == "Size": + mesh_x = int(mesh_child.get("X", default="1")) + mesh_y = int(mesh_child.get("Y", default="1")) + mesh_z = int(mesh_child.get("Z", default="1")) + mesh_size = (mesh_x, mesh_y, mesh_z) + if duration is None or output_time_step is None or mesh_size is None: + raise ValueError("Simulation element is missing required child elements") + simulation = vc.Simulation(name=name, duration=duration, output_time_step=output_time_step, mesh_size=mesh_size) + node.simulations.append(simulation) + def visit_Geometry(self, element: _Element, node: vc.Application) -> None: name: str = element.get("Name", default="unnamed") dim = int(element.get("Dimension", default="0")) diff --git a/pyvcell/vcml/vcml_writer.py b/pyvcell/vcml/vcml_writer.py index 53ad3ce..fbb7d52 100644 --- a/pyvcell/vcml/vcml_writer.py +++ b/pyvcell/vcml/vcml_writer.py @@ -36,11 +36,19 @@ def write_model(self, model: vc.Model, parent: _Element) -> None: parameter_element = Element("Parameter", Name=parameter.name, Role=parameter.role, Unit=parameter.unit) parameter_element.text = str(parameter.value) model_parameters_element.append(parameter_element) + for species in model.species: + species_type_element = Element("Compound", Name=species.name) + annotation_element = Element("Annotation") + annotation_element.text = species.name + species_type_element.append(annotation_element) + parent.append(species_type_element) for compartment in model.compartments: compartment_element = Element("Feature" if compartment.dim == 3 else "Membrane", Name=compartment.name) parent.append(compartment_element) for species in model.species: - species_element = Element("LocalizedCompound", Name=species.name, Structure=species.structure_name) + species_element = Element( + "LocalizedCompound", Name=species.name, CompoundRef=species.name, Structure=species.structure_name + ) parent.append(species_element) for reaction in model.reactions: reaction_element = Element("SimpleReaction", Structure=reaction.compartment_name, Name=reaction.name) @@ -73,6 +81,8 @@ def write_application(self, application: vc.Application, parent: _Element) -> No geometry_element = Element("Geometry", Name=application.geometry.name, Dimension=str(application.geometry.dim)) parent.append(geometry_element) self.write_geometry(application.geometry, geometry_element) + + # ---- geometry context ----- geometry_context_element = Element("GeometryContext") parent.append(geometry_context_element) for compartment_mapping in application.compartment_mappings: @@ -94,9 +104,13 @@ def write_application(self, application: vc.Application, parent: _Element) -> No ) mapping_element.append(boundaries_types_element) geometry_context_element.append(mapping_element) + + # ---- reaction context ----- + reaction_context_element = Element("ReactionContext") + parent.append(reaction_context_element) for species_mapping in application.species_mappings: mapping_element = Element("LocalizedCompoundSpec", LocalizedCompoundRef=species_mapping.species_name) - parent.append(mapping_element) + reaction_context_element.append(mapping_element) self.write_species_mapping(species_mapping, mapping_element) for reaction_mapping in application.reaction_mappings: mapping_element = Element( @@ -104,7 +118,50 @@ def write_application(self, application: vc.Application, parent: _Element) -> No ReactionStepRef=reaction_mapping.reaction_name, ReactionMapping="included" if reaction_mapping.included else "excluded", ) - parent.append(mapping_element) + reaction_context_element.append(mapping_element) + + # ---- mathDescription ---- (skip this for now) + math_description_element = Element("MathDescription", Name="dummy_math_description") + parent.append(math_description_element) + + # ---- simulations ----- + for simulation in application.simulations: + simulation_element = Element("Simulation", Name=simulation.name) + parent.append(simulation_element) + solver_task_description_element = Element( + "SolverTaskDescription", + TaskType="Unsteady", + UseSymbolicJacobian="false", + Solver="Sundials Stiff PDE Solver (Variable Time Step)", + ) + simulation_element.append(solver_task_description_element) + solver_task_description_element.append( + Element("TimeBound", StartTime="0.0", EndTime=str(simulation.duration)) + ) + solver_task_description_element.append( + Element("TimeStep", DefaultTime="0.05", MinTime="0.0", MaxTime="0.1") + ) + solver_task_description_element.append(Element("ErrorTolerance", Absolut="1.0E-9", Relative="1.0E-7")) + solver_task_description_element.append( + Element("OutputOptions", OutputTimeStep=str(simulation.output_time_step)) + ) + + sundials_solver_options_element = Element("SundialsSolverOptions") + max_order_advection_element = Element("maxOrderAdvection") + max_order_advection_element.text = "2" + sundials_solver_options_element.append(max_order_advection_element) + solver_task_description_element.append(sundials_solver_options_element) + number_processors_element = Element("NumberProcessors") + number_processors_element.text = "1" + solver_task_description_element.append(number_processors_element) + simulation_element.append(Element("MathOverrides")) + + mesh_specification_element = Element("MeshSpecification") + size_element = Element( + "Size", X=str(simulation.mesh_size[0]), Y=str(simulation.mesh_size[1]), Z=str(simulation.mesh_size[2]) + ) + mesh_specification_element.append(size_element) + simulation_element.append(mesh_specification_element) def write_geometry(self, geometry: vc.Geometry, parent: _Element) -> None: extent_element = Element( diff --git a/tests/conftest.py b/tests/conftest.py index 19bc9da..49efcc5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,3 @@ from tests.fixtures.data_fixtures import solver_output_path, solver_output_simid_jobid, zarr_path # noqa: F401 from tests.fixtures.model_fixtures import sbml_spatial_model_1d_path, sbml_spatial_model_3d_path # noqa: F401 -from tests.fixtures.vcell_model_fixtures import vcml_spatial_model_1d_path # noqa: F401 +from tests.fixtures.vcell_model_fixtures import vcml_spatial_model_1d_path, vcml_spatial_small_3d_path # noqa: F401 diff --git a/tests/data_model/test_simulations.py b/tests/data_model/test_simulations.py index 1e33912..c20452c 100644 --- a/tests/data_model/test_simulations.py +++ b/tests/data_model/test_simulations.py @@ -2,12 +2,14 @@ import numpy as np +import pyvcell.vcml as vc from pyvcell.data_model.result import Result -from pyvcell.data_model.simulation import SpatialSimulation +from pyvcell.data_model.simulation import SpatialSimulation, VcmlSpatialSimulation from pyvcell.data_model.spatial_model import SpatialModel +from pyvcell.data_model.vcml_spatial_model import VcmlSpatialModel -def test_model_parse_1d(sbml_spatial_model_1d_path: Path) -> None: +def test_sbml_model_parse_1d(sbml_spatial_model_1d_path: Path) -> None: assert sbml_spatial_model_1d_path.is_file() spatial_model = SpatialModel(filepath=sbml_spatial_model_1d_path) @@ -29,14 +31,16 @@ def test_model_parse_1d(sbml_spatial_model_1d_path: Path) -> None: assert spatial_model.get_coordinate_symbols() == ["x"] simulation_orig = SpatialSimulation(model=spatial_model) - results_orig: Result = simulation_orig.run(duration=5.0, output_time_step=0.1) + # results_orig: Result = simulation_orig.run(duration=5.0, output_time_step=0.1) + results_orig: Result = simulation_orig.run() spatial_model.set_parameter_value("Kr_r0", 100.0) assert spatial_model.model.getParameter("Kr_r0").getValue() == 100.0 spatial_model.set_parameter_value("s0_BC_Xm", 1.0) simulation_changed = SpatialSimulation(model=spatial_model) - results_changed: Result = simulation_changed.run(duration=5.0, output_time_step=0.1) + # results_changed: Result = simulation_changed.run(duration=5.0, output_time_step=0.1) + results_changed: Result = simulation_changed.run() channels_orig = results_orig.channels channels_changed = results_changed.channels @@ -65,7 +69,7 @@ def test_model_parse_1d(sbml_spatial_model_1d_path: Path) -> None: ) -def test_model_parse_3d(sbml_spatial_model_3d_path: Path) -> None: +def test_sbml_model_parse_3d(sbml_spatial_model_3d_path: Path) -> None: assert sbml_spatial_model_3d_path.is_file() spatial_model = SpatialModel(filepath=sbml_spatial_model_3d_path) @@ -114,13 +118,15 @@ def test_model_parse_3d(sbml_spatial_model_3d_path: Path) -> None: } simulation_orig = SpatialSimulation(model=spatial_model) - results_orig: Result = simulation_orig.run(duration=5.0, output_time_step=0.1) + # results_orig: Result = simulation_orig.run(duration=5.0, output_time_step=0.1) + results_orig: Result = simulation_orig.run() spatial_model.set_parameter_value("Kr_r0", 100.0) assert spatial_model.model.getParameter("Kr_r0").getValue() == 100.0 simulation_changed = SpatialSimulation(model=spatial_model) - results_changed: Result = simulation_changed.run(duration=5.0, output_time_step=0.1) + # results_changed: Result = simulation_changed.run(duration=5.0, output_time_step=0.1) + results_changed: Result = simulation_changed.run() channels_orig = results_orig.channels channels_changed = results_changed.channels @@ -170,3 +176,44 @@ def test_model_parse_3d(sbml_spatial_model_3d_path: Path) -> None: dtype=np.float64, ), ) + + +def test_vcml_model_parse_3d(vcml_spatial_model_1d_path: Path) -> None: + assert vcml_spatial_model_1d_path.is_file() + + vcml_spatial_model = VcmlSpatialModel(filepath=vcml_spatial_model_1d_path) + assert vcml_spatial_model is not None + parameters: list[vc.ModelParameter] = vcml_spatial_model.model_parameters + assert {p.name: p.value for p in parameters} == {"Kf_r0": 1.0, "Kr_r0": 0.5} + + sim_name = vcml_spatial_model.applications[0].simulations[0].name + simulation_orig = VcmlSpatialSimulation(model=vcml_spatial_model) + # # create a temporary file to write the VCML content to + # vcml_path = Path(tempfile.mktemp(prefix="model_", suffix=".xml")) + # vcml_spatial_model.export(vcml_path) + results_orig: Result = simulation_orig.run(simulation_name=sim_name) + + Kr_r0: vc.ModelParameter = next(p for p in vcml_spatial_model.model_parameters if p.name == "Kr_r0") + Kr_r0.value = 100.0 + + simulation_changed = VcmlSpatialSimulation(model=vcml_spatial_model) + results_changed: Result = simulation_changed.run(simulation_name=sim_name) + + channels_orig = results_orig.channels + channels_changed = results_changed.channels + assert [channel.label for channel in channels_orig] == ["s0", "s1", "J_r0", "s0_init_uM", "s1_init_uM"] + assert [channel.label for channel in channels_changed] == ["s0", "s1", "J_r0", "s0_init_uM", "s1_init_uM"] + assert np.allclose( + results_orig.concentrations[0, 0::10], + np.array( + [500000.00000000006, 236185.00811512678, 111567.6111715472], + dtype=np.float64, + ), + ) + assert np.allclose( + results_changed.concentrations[0, 0::10], + np.array( + [500000.00000000006, 9.900990099017005, 9.900990099052574], + dtype=np.float64, + ), + ) diff --git a/tests/fixtures/data/SmallSpacialProject_3D.vcml b/tests/fixtures/data/SmallSpacialProject_3D.vcml new file mode 100644 index 0000000..0048bcc --- /dev/null +++ b/tests/fixtures/data/SmallSpacialProject_3D.vcml @@ -0,0 +1,184 @@ + + + + + + + 1.0 + 0.5 + 1.0 + 1.0 + 1.0 + 1.0 + + + s0 + + + s1 + + + s2 + + + s3 + + + + + + + + + + + + + ((Kf_r0 * s0) - (1.0 * Kr_r0 * s1)) + + + + + + + ((Kf_r1 * s0) - (Kr_r1 * s2)) + 0.0 + 1.0 + + + + + + + ((Kf_r2 * s2) - (Kr_r2 * s3)) + 0.0 + 1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ((pow((-5.0 + x),2.0) + pow((-5.0 + y),2.0) + pow((-5.0 + z),2.0)) < 16.0) + + + 1.0 + + + + + + + + + + + + + + + + + + + + + + (1.0 + sin(x)) + 1.0E-4 + + + + (1.0 + cos(x)) + 1.0E-4 + + + + (1.0 + (sin(x) * cos(y))) + 1.0E-4 + + + + (1.0 + cos(y)) + 1.0000000000000002E-6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/data/TinySpatialProject_Application0.vcml b/tests/fixtures/data/TinySpatialProject_Application0.vcml index 9328bdb..0facf6a 100644 --- a/tests/fixtures/data/TinySpatialProject_Application0.vcml +++ b/tests/fixtures/data/TinySpatialProject_Application0.vcml @@ -1,6 +1,6 @@ - - + + @@ -13,125 +13,53 @@ s1 - - - - - - - ((Kf_r0 * s0) - (1.0 * Kr_r0 * s1)) + ((Kf_r0 * s0) - (Kr_r0 * s1)) - - - - - ((Kf * s0) - (Kr * s2)) - 0.0 - 1.0 - 1.0 - 1.0 - - - - - - - ((Kf * s2) - (Kr * s3)) - 0.0 - 1.0 - 1.0 - 1.0 - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - + + - - ((((-5.0 + x) ^ 2.0) + ((-5.0 + y) ^ 2.0) + ((-5.0 + z) ^ 2.0)) < 16.0) - 1.0 - - - - - + + - - - - - - - (1.0 + sin(x)) - 1.0E-4 + (100000.0 * x) + 1.0E-9 - (1.0 + cos(x)) - 1.0E-4 + (10.0 - (1.0 * 100000.0 * x)) + 1.0E-9 - - (1.0 + (sin(x) * cos(y))) - 1.0E-4 - - - (1.0 + cos(y)) - 1.0000000000000002E-6 - - - 96485.3321 @@ -141,67 +69,31 @@ 3.141592653589793 8314.46261815 300.0 - 1.0 1000.0 1.0 - 1.0 - 1.0 0.001660538783162726 0.5 - 1.0 - 1.0 - 1.0 - 1.0 0.0 0.0 - 1.0E-4 + 1.0E-9 0.0 0.0 - 1.0E-4 - 1.0000000000000002E-6 - 1.0E-4 - 0.0 + 1.0E-9 1.0 - 1.0 - - - ((Kf_r0 * s0) - (1.0 * Kr_r0 * s1)) - ((Kf_r1 * s0) - (Kr_r1 * s2)) - ((Kf_r2 * s2) - (Kr_r2 * s3)) - (AreaPerUnitArea_m0 / VolumePerUnitVolume_c0) - (AreaPerUnitArea_m0 / VolumePerUnitVolume_c1) - (1.0 + sin(x)) - (1.0 + cos(x)) - (1.0 + cos(y)) - (1.0 + (sin(x) * cos(y))) + ((Kf_r0 * s0) - (Kr_r0 * s1)) + (100000.0 * x) + (10.0 - (1.0 * 100000.0 * x)) (VolumePerUnitVolume_c0 * vcRegionVolume('subdomain0')) - (VolumePerUnitVolume_c1 * vcRegionVolume('subdomain1')) - (AreaPerUnitArea_m0 * vcRegionArea('subdomain0_subdomain1_membrane')) - vcRegionArea('subdomain0_subdomain1_membrane') vcRegionVolume('subdomain0') - vcRegionVolume('subdomain1') - - - - - - - - - 0.0 - s3_diffusionRate - s3_init_umol_l_1 - - - - - - + + + + - J_r0 @@ -215,38 +107,13 @@ s1_init_umol_l_1 - - - - - - - - - (J_r1 - J_r2) - s2_diffusionRate - s2_init_umol_dm_2 - - - ( - 1.0 * KFlux_m0_c0 * J_r1) - 0.0 - - - 0.0 - 0.0 - - - 0.0 - (KFlux_m0_c1 * J_r2) - - - + - + 2 @@ -254,7 +121,7 @@ - + @@ -265,22 +132,6 @@ - - - - - - - - - - - - - - - - diff --git a/tests/fixtures/data/TinySpatialProject_Application0_from_python.vcml b/tests/fixtures/data/TinySpatialProject_Application0_from_python.vcml new file mode 100644 index 0000000..bd032c1 --- /dev/null +++ b/tests/fixtures/data/TinySpatialProject_Application0_from_python.vcml @@ -0,0 +1,70 @@ + + + + + + 1.0 + 0.5 + + + s0 + + + s1 + + + + + + + + + ((Kf_r0 * s0) - (Kr_r0 * s1)) + + + + + + + + + 1.0 + + + + + + + + + + (100000.0 * x) + 1e-09 + + + + (10.0 - (1.0 * 100000.0 * x)) + 1e-09 + + + + + + + + + + + + 2 + + 1 + + + + + + + + + diff --git a/tests/fixtures/vcell_model_fixtures.py b/tests/fixtures/vcell_model_fixtures.py index 9f035f6..751a2d7 100644 --- a/tests/fixtures/vcell_model_fixtures.py +++ b/tests/fixtures/vcell_model_fixtures.py @@ -9,3 +9,8 @@ @pytest.fixture def vcml_spatial_model_1d_path() -> Path: return FIXTURE_DATA_DIR / "TinySpatialProject_Application0.vcml" + + +@pytest.fixture +def vcml_spatial_small_3d_path() -> Path: + return FIXTURE_DATA_DIR / "SmallSpacialProject_3D.vcml" diff --git a/tests/vcml/test_vcml_reader.py b/tests/vcml/test_vcml_reader.py index 44d1b74..82ed4af 100644 --- a/tests/vcml/test_vcml_reader.py +++ b/tests/vcml/test_vcml_reader.py @@ -3,7 +3,7 @@ import pyvcell.vcml as vc -def test_vcml_reader(vcml_spatial_model_1d_path: Path) -> None: +def test_vcml_reader_1D(vcml_spatial_model_1d_path: Path) -> None: assert vcml_spatial_model_1d_path.is_file() with open(vcml_spatial_model_1d_path) as f: @@ -15,6 +15,61 @@ def test_vcml_reader(vcml_spatial_model_1d_path: Path) -> None: assert model is not None and model.name == "unnamed" assert [p.name for p in model.model_parameters] == ["Kf_r0", "Kr_r0"] + assert [r.name for r in model.reactions] == ["r0"] + assert [(c.name, c.dim) for c in model.compartments] == [("c0", 3)] + assert [(s.name, s.structure_name) for s in model.species] == [ + ("s0", "c0"), + ("s1", "c0"), + ] + + r0: vc.Reaction = model.reactions[0] + assert [(r.name, r.stoichiometry) for r in r0.reactants] == [("s0", 1)] + assert [(p.name, p.stoichiometry) for p in r0.products] == [("s1", 1)] + assert r0.kinetics is not None and r0.kinetics.kinetics_type == "GeneralKinetics" + assert r0.compartment_name == "c0" + assert {p.name: p.value for p in r0.kinetics.kinetics_parameters} == {"J": "((Kf_r0 * s0) - (Kr_r0 * s1))"} + + assert [a.name for a in biomodel.applications] == ["unnamed_spatialGeom"] + app0 = biomodel.applications[0] + assert app0.stochastic is False + + geom = app0.geometry + assert (geom.name, geom.dim) == ("spatialGeom", 1) + assert geom.extent == (10.0, 1.0, 1.0) + assert geom.origin == (0.0, 0.0, 0.0) + assert [(sv.name, sv.handle, sv.subvolume_type.name, sv.analytic_expr) for sv in geom.subvolumes] == [ + ("subdomain0", 0, "analytic", "1.0") + ] + assert [(sc.name, sc.subvolume_ref_1, sc.subvolume_ref_2) for sc in geom.surface_classes] == [] + + assert [ + (cm.compartment_name, cm.geometry_class_name, cm.unit_size, cm.boundary_types) + for cm in app0.compartment_mappings + ] == [("c0", "subdomain0", 1.0, ["flux", "flux", "flux", "flux", "flux", "flux"])] + + assert [ + (sm.species_name, sm.diffusion_coefficient, sm.initial_concentration, sm.boundary_values) + for sm in app0.species_mappings + ] == [ + ("s0", 1e-09, "(100000.0 * x)", [0.0, 0.0, None, None, None, None]), + ("s1", 1e-09, "(10.0 - (1.0 * 100000.0 * x))", [0.0, 0.0, None, None, None, None]), + ] + + assert [[(rm.reaction_name, rm.included) for rm in app0.reaction_mappings]] == [[("r0", True)]] + + +def test_vcml_reader_3D(vcml_spatial_small_3d_path: Path) -> None: + assert vcml_spatial_small_3d_path.is_file() + + with open(vcml_spatial_small_3d_path) as f: + xml_string = f.read() + + biomodel = vc.VcmlReader.parse_biomodel(xml_string) + assert biomodel is not None and biomodel.name == "TinySpacialProject_Application0_unnamed_spatialGeom" + model = biomodel.model + assert model is not None and model.name == "unnamed" + + assert [p.name for p in model.model_parameters] == ["Kf_r0", "Kr_r0", "Kf_r1", "Kr_r1", "Kf_r2", "Kr_r2"] assert [r.name for r in model.reactions] == ["r0", "r1", "r2"] assert [(c.name, c.dim) for c in model.compartments] == [("c0", 3), ("c1", 3), ("m0", 2)] assert [(s.name, s.structure_name) for s in model.species] == [ @@ -34,27 +89,23 @@ def test_vcml_reader(vcml_spatial_model_1d_path: Path) -> None: r1: vc.Reaction = model.reactions[1] assert [(r.name, r.stoichiometry) for r in r1.reactants] == [("s0", 1)] assert [(p.name, p.stoichiometry) for p in r1.products] == [("s2", 1)] - assert r1.kinetics is not None and r1.kinetics.kinetics_type == "MassAction" + assert r1.kinetics is not None and r1.kinetics.kinetics_type == "GeneralKinetics" assert r1.compartment_name == "m0" assert {p.name: p.value for p in r1.kinetics.kinetics_parameters} == { - "J": "((Kf * s0) - (Kr * s2))", - "I": 0, - "netValence": 1, - "Kf": 1, - "Kr": 1, + "I": 0.0, + "J": "((Kf_r1 * s0) - (Kr_r1 * s2))", + "netValence": 1.0, } r2: vc.Reaction = model.reactions[2] assert [(r.name, r.stoichiometry) for r in r2.reactants] == [("s2", 1)] assert [(p.name, p.stoichiometry) for p in r2.products] == [("s3", 1)] - assert r2.kinetics is not None and r2.kinetics.kinetics_type == "MassAction" + assert r2.kinetics is not None and r2.kinetics.kinetics_type == "GeneralKinetics" assert r2.compartment_name == "m0" assert {p.name: p.value for p in r2.kinetics.kinetics_parameters} == { - "J": "((Kf * s2) - (Kr * s3))", - "I": 0, - "netValence": 1, - "Kf": 1, - "Kr": 1, + "I": 0.0, + "J": "((Kf_r2 * s2) - (Kr_r2 * s3))", + "netValence": 1.0, } assert [a.name for a in biomodel.applications] == ["unnamed_spatialGeom"] @@ -62,11 +113,11 @@ def test_vcml_reader(vcml_spatial_model_1d_path: Path) -> None: assert app0.stochastic is False geom = app0.geometry - assert (geom.name, geom.dim) == ("Geometry3", 3) + assert (geom.name, geom.dim) == ("spatialGeom", 3) assert geom.extent == (10.0, 10.0, 10.0) assert geom.origin == (0.0, 0.0, 0.0) assert [(sv.name, sv.handle, sv.subvolume_type.name, sv.analytic_expr) for sv in geom.subvolumes] == [ - ("subdomain1", 1, "analytic", "((((-5.0 + x) ^ 2.0) + ((-5.0 + y) ^ 2.0) + ((-5.0 + z) ^ 2.0)) < 16.0)"), + ("subdomain1", 1, "analytic", "((pow((-5.0 + x),2.0) + pow((-5.0 + y),2.0) + pow((-5.0 + z),2.0)) < 16.0)"), ("subdomain0", 0, "analytic", "1.0"), ] assert [(sc.name, sc.subvolume_ref_1, sc.subvolume_ref_2) for sc in geom.surface_classes] == [ @@ -86,10 +137,10 @@ def test_vcml_reader(vcml_spatial_model_1d_path: Path) -> None: (sm.species_name, sm.diffusion_coefficient, sm.initial_concentration, sm.boundary_values) for sm in app0.species_mappings ] == [ - ("s0", 0.0001, "(1.0 + sin(x))", [0.0, 0.0, None, None, None, None]), - ("s1", 0.0001, "(1.0 + cos(x))", [0.0, 0.0, None, None, None, None]), - ("s3", 0.0001, "(1.0 + (sin(x) * cos(y)))", []), - ("s2", 1.0000000000000002e-06, "(1.0 + cos(y))", []), + ("s0", 0.0001, "(1.0 + sin(x))", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + ("s1", 0.0001, "(1.0 + cos(x))", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + ("s3", 0.0001, "(1.0 + (sin(x) * cos(y)))", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + ("s2", 1.0000000000000002e-06, "(1.0 + cos(y))", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ] assert [[(rm.reaction_name, rm.included) for rm in app0.reaction_mappings]] == [ diff --git a/tests/vcml/test_vcml_writer.py b/tests/vcml/test_vcml_writer.py index ee8db84..68f72eb 100644 --- a/tests/vcml/test_vcml_writer.py +++ b/tests/vcml/test_vcml_writer.py @@ -3,7 +3,7 @@ import pyvcell.vcml as vc -def test_vcml_writer(vcml_spatial_model_1d_path: Path) -> None: +def test_vcml_writer_1D(vcml_spatial_model_1d_path: Path) -> None: assert vcml_spatial_model_1d_path.is_file() with open(vcml_spatial_model_1d_path) as f: @@ -41,3 +41,43 @@ def test_vcml_writer(vcml_spatial_model_1d_path: Path) -> None: f.write(new_vcml_str) assert biomodel == new_biomodel + + +def test_vcml_writer_3D(vcml_spatial_small_3d_path: Path) -> None: + assert vcml_spatial_small_3d_path.is_file() + + with open(vcml_spatial_small_3d_path) as f: + orig_vcml_string = f.read() + + biomodel = vc.VcmlReader.parse_biomodel(orig_vcml_string) + document = vc.VCMLDocument(biomodel=biomodel) + + new_vcml_str: str = vc.VcmlWriter().write_vcml(document=document) + assert new_vcml_str is not None + + new_biomodel = vc.VcmlReader.parse_biomodel(new_vcml_str) + + assert biomodel is not None and new_biomodel is not None + assert biomodel.name == new_biomodel.name + assert biomodel.model is not None and new_biomodel.model is not None + assert biomodel.model.name == new_biomodel.model.name + assert biomodel.model.model_parameters == new_biomodel.model.model_parameters + + for i, reaction in enumerate(biomodel.model.reactions): + new_reaction = new_biomodel.model.reactions[i] + assert reaction.name == new_reaction.name + assert reaction.reactants == new_reaction.reactants + assert reaction.products == new_reaction.products + assert reaction.kinetics == new_reaction.kinetics + + for i, compartment in enumerate(biomodel.model.compartments): + new_compartment = new_biomodel.model.compartments[i] + assert compartment.name == new_compartment.name + assert compartment.dim == new_compartment.dim + + # write out the new vcml string to a file so that it can be compared with original vcml file + new_vcml_path = vcml_spatial_small_3d_path.with_name("new_vcml_3d.xml") + with open(new_vcml_path, "w") as f: + f.write(new_vcml_str) + + assert biomodel == new_biomodel