From 6987aed6998e5dae99e21247a3d11a4f1856da6f Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 24 Jun 2026 01:08:47 -0400 Subject: [PATCH 1/3] Add moving-boundary solver support Author and run VCell moving-boundary simulations from pyvcell via libvcell (>= 0.0.16) and the pyvcell-mbsolver package. Authoring API (models_app): - Application.add_moving_boundary_sim(): a simulation with Solver="MovingB" and MovingBoundarySolverOptions. - Application.set_moving_boundary_front(): the prescribed front velocity (FrontVelocity / SurfaceKinematics). - Simulation.solver / .moving_boundary_options / .is_moving_boundary. Writer/reader round-trip the Solver attribute and MovingBoundarySolverOptions. Runtime: - vc.simulate_moving_boundary(biomodel, sim) drives the pipeline: to_vcml_str (round-trip 1 -> VCell auto-generates SpatialObjects) -> inject SurfaceKinematics front velocity -> vcml_to_vcml (round-trip 2 -> moving-boundary math) -> vcml_to_moving_boundary_input -> run pyvcell_mbsolver. - Output is collected via solver observers (no HDF5 file is written by the binding) into MovingBoundaryResult: per output time, the moving front polygon plus per-element (x, y, grid_i, grid_j, concentration) on the moving mesh, subsampled to output_time_step. Packaging: new `mb` extra (pyvcell-mbsolver, gated to Python < 3.14), added to `all`; lazy imports with a clear "install pyvcell[mb]" hint. The `native` extra is held at >=0.0.15.3 until libvcell 0.0.16 is published to PyPI, so a release should wait for that even though the PR can merge. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/getting-started/installation.md | 8 +- pyproject.toml | 17 +- pyvcell/_internal/solvers/mbsolver.py | 118 +++++++++++ pyvcell/sim_results/moving_boundary_result.py | 73 +++++++ pyvcell/vcml/__init__.py | 11 + pyvcell/vcml/models.py | 6 + pyvcell/vcml/models_app.py | 90 +++++++++ pyvcell/vcml/vcml_mb_simulation.py | 190 ++++++++++++++++++ pyvcell/vcml/vcml_reader.py | 19 ++ pyvcell/vcml/vcml_writer.py | 26 ++- tests/vcml/test_moving_boundary.py | 117 +++++++++++ uv.lock | 27 ++- 12 files changed, 691 insertions(+), 11 deletions(-) create mode 100644 pyvcell/_internal/solvers/mbsolver.py create mode 100644 pyvcell/sim_results/moving_boundary_result.py create mode 100644 pyvcell/vcml/vcml_mb_simulation.py create mode 100644 tests/vcml/test_moving_boundary.py diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d73ad02b..0bb3292f 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -15,9 +15,15 @@ features live behind optional extras — install the ones you need, or everythin ```bash pip install "pyvcell[viz]" # plotting / VTK / PyVista -pip install "pyvcell[all]" # full feature set (solver, viz, remote, io, convert, native) +pip install "pyvcell[mb]" # moving-boundary solver (pyvcell-mbsolver) +pip install "pyvcell[all]" # full feature set (solver, viz, remote, io, convert, native, mb) ``` +> The moving-boundary solver (`mb`) additionally needs `libvcell >= 0.0.16` to +> author its solver input; until that release is on PyPI, install `libvcell` +> 0.0.16 from a local wheel. `pyvcell-mbsolver` currently has no Python 3.14 +> wheel, so the `mb` extra is skipped on Python 3.14. + ## Install for development (uv) ```bash diff --git a/pyproject.toml b/pyproject.toml index 829cf606..96596e78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,8 +61,16 @@ convert = [ native = [ "libvcell (>=0.0.15.3)", ] +# Moving-boundary solver. Authoring moving-boundary input additionally requires +# libvcell >= 0.0.16 (the `native` extra is held at >=0.0.15.3 until 0.0.16 is +# published to PyPI). pyvcell-mbsolver has no cp314 wheel yet, so it is gated to +# Python < 3.14. +mb = [ + "pyvcell-mbsolver>=1.0.0,<2 ; python_version < '3.14'", + "pyvcell[native]", +] all = [ - "pyvcell[solver,viz,remote,io,convert,native]", + "pyvcell[solver,viz,remote,io,convert,native,mb]", ] [dependency-groups] @@ -131,6 +139,13 @@ disable_error_code = [ "import-untyped" ] +[[tool.mypy.overrides]] +module = "pyvcell_mbsolver.*" +ignore_missing_imports = true +disable_error_code = [ + "import-untyped" +] + [[tool.mypy.overrides]] module = "trame.*" ignore_missing_imports = true diff --git a/pyvcell/_internal/solvers/mbsolver.py b/pyvcell/_internal/solvers/mbsolver.py new file mode 100644 index 00000000..f87d35c6 --- /dev/null +++ b/pyvcell/_internal/solvers/mbsolver.py @@ -0,0 +1,118 @@ +"""Thin wrapper around the ``pyvcell_mbsolver`` moving-boundary solver. + +The solver reports its solution through observer callbacks rather than an output +file: once per internal time step it hands back the moving front geometry and +the per-element field values. :func:`solve_moving_boundary` runs the solver with +a collecting observer that snapshots those callbacks at the requested output +times into a :class:`~pyvcell.sim_results.moving_boundary_result.MovingBoundaryResult`. + +Callers who need full control can use ``pyvcell_mbsolver`` directly; this wrapper +covers the common "run it and give me the trajectory" case. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from os import PathLike +from typing import Any + +import numpy as np +import pyvcell_mbsolver as mb + +from pyvcell.sim_results.moving_boundary_result import MovingBoundaryFrame, MovingBoundaryResult + +# Time tolerance (relative to the output step) for deciding a step has reached an +# output time; the solver advances on its own internal step, not the output step. +_OUTPUT_TIME_TOL = 1e-9 + + +def solve_moving_boundary( + setup_xml_file: PathLike[str] | str, + species_names: Sequence[str], + output_times: Sequence[float] | None = None, +) -> MovingBoundaryResult: + """Run the moving-boundary solver on a ``MovingBoundarySetup`` XML file. + + Args: + setup_xml_file: the ``*_mb.xml`` setup produced by + ``libvcell.vcml_to_moving_boundary_input``. + species_names: the volume species, in the order the solver reports + concentrations (i.e. the order of the ```` entries in the + setup file's ```` block). + output_times: times at which to snapshot a frame. The first solver step + at or past each requested time is kept; the final step is always + kept. When None, every solver step is kept. + + Returns: + a :class:`MovingBoundaryResult` with one frame per kept output time. + """ + species = list(species_names) + targets = sorted(float(t) for t in output_times) if output_times is not None else None + result = MovingBoundaryResult(species_names=species) + + class _Collector(mb.SimulationObserver): # type: ignore[misc] + def __init__(self) -> None: + super().__init__() + self._target_index = 0 + self._keep = False + self._time = 0.0 + self._front: np.ndarray = np.empty((0, 2), dtype=float) + self._buffer: list[tuple[float, float, int, int, list[float]]] = [] + + def on_time(self, t: float, generation: int, last: bool, geometry: Any) -> None: + keep = bool(last) + if targets is None: + keep = True + else: + tol = _OUTPUT_TIME_TOL * (targets[-1] - targets[0] + 1.0) + while self._target_index < len(targets) and t + tol >= targets[self._target_index]: + keep = True + self._target_index += 1 + self._keep = keep + if keep: + self._time = t + boundary = list(getattr(geometry, "boundary", []) or []) + self._front = np.array(boundary, dtype=float).reshape(-1, 2) if boundary else np.empty((0, 2)) + self._buffer = [] + + def on_element(self, node: Any) -> None: + if not self._keep or node.is_outside: + return + self._buffer.append(( + float(node.x), + float(node.y), + int(node.grid_i), + int(node.grid_j), + [float(node.concentration(i)) for i in range(len(species))], + )) + + def on_iteration_complete(self) -> None: + if not self._keep: + return + self._keep = False + x = np.array([row[0] for row in self._buffer], dtype=float) + y = np.array([row[1] for row in self._buffer], dtype=float) + grid_i = np.array([row[2] for row in self._buffer], dtype=int) + grid_j = np.array([row[3] for row in self._buffer], dtype=int) + concentrations = { + name: np.array([row[4][i] for row in self._buffer], dtype=float) for i, name in enumerate(species) + } + result.frames.append( + MovingBoundaryFrame( + time=self._time, + front=self._front, + x=x, + y=y, + grid_i=grid_i, + grid_j=grid_j, + concentrations=concentrations, + ) + ) + + def on_complete(self) -> None: + pass + + solver = mb.MovingBoundarySolver.from_xml(str(setup_xml_file)) + solver.add_element_observer(_Collector(), name="pyvcell_collector") + solver.run() + return result diff --git a/pyvcell/sim_results/moving_boundary_result.py b/pyvcell/sim_results/moving_boundary_result.py new file mode 100644 index 00000000..2c532d1e --- /dev/null +++ b/pyvcell/sim_results/moving_boundary_result.py @@ -0,0 +1,73 @@ +"""Result of a moving-boundary simulation. + +Unlike the finite-volume solver, the moving-boundary solver does not write a +fixed Cartesian grid. Its solution lives on a mesh that moves with the boundary, +so a result is a time series of frames, where each frame carries the moving +front (a polygon of ``(x, y)`` points) and the field values of the "inside" +mesh elements at that time. This mirrors VCell's own moving-boundary data model +(elements + per-species values on the moving mesh), rather than the +zarr/4-D Cartesian :class:`~pyvcell.sim_results.result.Result`. + +These objects hold only numpy arrays and plain Python, so they can be inspected +and post-processed without any heavy optional dependency. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + + +@dataclass +class MovingBoundaryFrame: + """A single output time step of a moving-boundary simulation. + + Attributes: + time: the simulation time of this frame. + front: ``(N, 2)`` array of the moving-front boundary polygon ``(x, y)``. + x, y: ``(M,)`` coordinates of the inside mesh elements. + grid_i, grid_j: ``(M,)`` integer grid indices of the inside elements. + concentrations: species name -> ``(M,)`` array of element concentrations, + aligned with ``x``/``y``/``grid_i``/``grid_j``. + """ + + time: float + front: np.ndarray + x: np.ndarray + y: np.ndarray + grid_i: np.ndarray + grid_j: np.ndarray + concentrations: dict[str, np.ndarray] = field(default_factory=dict) + + +@dataclass +class MovingBoundaryResult: + """The full time series of a moving-boundary simulation. + + Attributes: + species_names: the volume species, in the same order the solver reports + concentrations. + frames: the output-time frames, in increasing time order. + """ + + species_names: list[str] + frames: list[MovingBoundaryFrame] = field(default_factory=list) + + @property + def times(self) -> np.ndarray: + """The output times as a 1-D array.""" + return np.array([frame.time for frame in self.frames], dtype=float) + + def front(self, time_index: int) -> np.ndarray: + """The moving-front polygon ``(N, 2)`` at the given output index.""" + return self.frames[time_index].front + + def concentrations(self, species_name: str, time_index: int) -> np.ndarray: + """Inside-element concentrations of ``species_name`` at the given output index.""" + return self.frames[time_index].concentrations[species_name] + + def __repr__(self) -> str: + n = len(self.frames) + span = f"{self.frames[0].time:g}..{self.frames[-1].time:g}" if n else "empty" + return f"MovingBoundaryResult(species={self.species_names}, frames={n}, t={span})" diff --git a/pyvcell/vcml/__init__.py b/pyvcell/vcml/__init__.py index 86840b5a..dda84f9a 100644 --- a/pyvcell/vcml/__init__.py +++ b/pyvcell/vcml/__init__.py @@ -21,10 +21,12 @@ Biomodel, BoundaryType, Compartment, + FrontVelocity, Kinetics, KineticsParameter, Model, ModelParameter, + MovingBoundarySolverOptions, Reaction, Simulation, Species, @@ -68,6 +70,7 @@ if TYPE_CHECKING: # Heavy names — eager only for type checkers / IDE autocomplete. from pyvcell._internal.geometry import SegmentedImageGeometry + from pyvcell.sim_results.moving_boundary_result import MovingBoundaryResult from pyvcell.vcml.field import Field from pyvcell.vcml.session import SimulationJob, VCellSession from pyvcell.vcml.utils import ( @@ -90,6 +93,7 @@ write_sbml_file, write_vcml_file, ) + from pyvcell.vcml.vcml_mb_simulation import simulate_moving_boundary from pyvcell.vcml.vcml_remote import connect, logout from pyvcell.vcml.vcml_simulation import cartesian_mesh_from_geometry, simulate from pyvcell.vcml.vcml_writer import VcmlWriter @@ -104,6 +108,8 @@ **dict.fromkeys(["SimulationJob", "VCellSession"], "pyvcell.vcml.session"), **dict.fromkeys(["connect", "logout"], "pyvcell.vcml.vcml_remote"), **dict.fromkeys(["simulate", "cartesian_mesh_from_geometry"], "pyvcell.vcml.vcml_simulation"), + "simulate_moving_boundary": "pyvcell.vcml.vcml_mb_simulation", + "MovingBoundaryResult": "pyvcell.sim_results.moving_boundary_result", **dict.fromkeys(["get_workspace_dir", "set_workspace_dir"], "pyvcell.vcml.workspace"), **dict.fromkeys( [ @@ -135,6 +141,7 @@ "libvcell": "native", "pyvcell_fvsolver": "solver", "fvsolver": "solver", + "pyvcell_mbsolver": "mb", "vtk": "viz", "pyvista": "viz", "matplotlib": "viz", @@ -192,6 +199,7 @@ def __dir__() -> list[str]: "Constant", "Effect", "Field", + "FrontVelocity", "Geometry", "Image", "JumpCondition", @@ -206,6 +214,8 @@ def __dir__() -> list[str]: "MembraneSubDomain", "Model", "ModelParameter", + "MovingBoundaryResult", + "MovingBoundarySolverOptions", "OdeEquation", "ParticleInitialCount", "ParticleJumpProcess", @@ -246,6 +256,7 @@ def __dir__() -> list[str]: "restore_stdout", "set_workspace_dir", "simulate", + "simulate_moving_boundary", "suppress_stdout", "to_antimony_str", "to_sbml_str", diff --git a/pyvcell/vcml/models.py b/pyvcell/vcml/models.py index 284b050f..c591ec54 100644 --- a/pyvcell/vcml/models.py +++ b/pyvcell/vcml/models.py @@ -14,6 +14,12 @@ from pyvcell.vcml.models_app import ( CompartmentMapping as CompartmentMapping, ) +from pyvcell.vcml.models_app import ( + FrontVelocity as FrontVelocity, +) +from pyvcell.vcml.models_app import ( + MovingBoundarySolverOptions as MovingBoundarySolverOptions, +) from pyvcell.vcml.models_app import ( ReactionMapping as ReactionMapping, ) diff --git a/pyvcell/vcml/models_app.py b/pyvcell/vcml/models_app.py index eeafa1aa..41953b86 100644 --- a/pyvcell/vcml/models_app.py +++ b/pyvcell/vcml/models_app.py @@ -26,10 +26,47 @@ from pyvcell.vcml.models import Compartment, Reaction, Species +# VCell database solver names (the ``Solver`` attribute of ``SolverTaskDescription``). +DEFAULT_SOLVER = "Sundials Stiff PDE Solver (Variable Time Step)" +MOVING_BOUNDARY_SOLVER = "MovingB" + + class ApplicationParameter(Parameter): pass +class MovingBoundarySolverOptions(VcmlNode): + """Options for the Moving Boundary solver (````). + + Defaults match a typical VCell moving-boundary simulation. The string-valued + options use VCell's enum names: ``redistribution_mode`` is one of + ``NO_REDIST``/``EXPANSION_REDIST``/``FULL_REDIST``; ``redistribution_version`` + is ``ORDINARY_REDISTRIBUTE``/``EQUI_BOND_REDISTRIBUTE`` (only meaningful for + ``FULL_REDIST``); ``extrapolation_method`` is ``NEAREST_NEIGHBOR``. + """ + + front_to_node_ratio: float = 1.0 + redistribution_mode: str = "FULL_REDIST" + redistribution_version: str = "EQUI_BOND_REDISTRIBUTE" + redistribution_frequency: int = 5 + extrapolation_method: str = "NEAREST_NEIGHBOR" + + +class FrontVelocity(VcmlNode): + """Prescribed velocity of a moving-boundary front (a ``SurfaceKinematics`` process). + + The velocity components are VCell expressions that may depend on space + (``x``, ``y``, ``z``), time (``t``), and the volume species. ``surface_name`` + is the geometry surface class that moves; when omitted, the application's + single surface class is used. + """ + + velocity_x: float | str = 0.0 + velocity_y: float | str = 0.0 + velocity_z: float | str = 0.0 + surface_name: str | None = None + + class StructureMapping(VcmlNode): structure_name: str geometry_class: GeometryClass @@ -90,8 +127,14 @@ class Simulation(VcmlNode): duration: float output_time_step: float mesh_size: tuple[int, int, int] + solver: str = DEFAULT_SOLVER + moving_boundary_options: MovingBoundarySolverOptions | None = None version: Version | None = None + @property + def is_moving_boundary(self) -> bool: + return self.solver == MOVING_BOUNDARY_SOLVER + @property def mesh_array_shape(self) -> tuple[int, ...]: if self.mesh_size[1] == 1 and self.mesh_size[2] == 1: @@ -120,6 +163,7 @@ class Application(VcmlNode): output_functions: list[AnnotatedFunction] = Field(default_factory=list) simulations: list[Simulation] = Field(default_factory=list) application_parameters: list[ApplicationParameter] = Field(default_factory=list) + front_velocity: FrontVelocity | None = None math_description: MathDescription | None = None def __repr__(self) -> str: @@ -169,3 +213,49 @@ def add_sim( sim = Simulation(name=name, duration=duration, output_time_step=output_time_step, mesh_size=mesh_size) self.simulations.append(sim) return sim + + def set_moving_boundary_front( + self, + velocity_x: float | str = 0.0, + velocity_y: float | str = 0.0, + velocity_z: float | str = 0.0, + surface_name: str | None = None, + ) -> FrontVelocity: + """Declare the prescribed velocity of the moving-boundary front. + + The front is the geometry surface that separates the two subvolumes of a + moving-boundary model. The velocity components are VCell expressions in + space (``x``, ``y``, ``z``), time (``t``), and the volume species. When + ``surface_name`` is omitted, the geometry's single surface class is used. + """ + front = FrontVelocity( + velocity_x=velocity_x, velocity_y=velocity_y, velocity_z=velocity_z, surface_name=surface_name + ) + self.front_velocity = front + return front + + def add_moving_boundary_sim( + self, + name: str, + duration: float, + output_time_step: float, + mesh_size: tuple[int, int, int], + options: MovingBoundarySolverOptions | None = None, + ) -> Simulation: + """Add a simulation configured for the Moving Boundary solver. + + Equivalent to :meth:`add_sim` but sets the solver to ``MovingB`` and + attaches :class:`MovingBoundarySolverOptions` (defaults when ``options`` + is None). The application must also declare a moving front via + :meth:`set_moving_boundary_front`. + """ + sim = Simulation( + name=name, + duration=duration, + output_time_step=output_time_step, + mesh_size=mesh_size, + solver=MOVING_BOUNDARY_SOLVER, + moving_boundary_options=options or MovingBoundarySolverOptions(), + ) + self.simulations.append(sim) + return sim diff --git a/pyvcell/vcml/vcml_mb_simulation.py b/pyvcell/vcml/vcml_mb_simulation.py new file mode 100644 index 00000000..f41e0bb5 --- /dev/null +++ b/pyvcell/vcml/vcml_mb_simulation.py @@ -0,0 +1,190 @@ +"""Author and run a moving-boundary simulation. + +The moving-boundary path differs from the finite-volume path in two ways: the +solver math (a moving membrane front with a prescribed velocity) is generated by +libvcell from application-level ``SpatialObjects``/``SpatialProcesses``, and the +result lives on a moving mesh rather than a fixed Cartesian grid. + +:func:`simulate_moving_boundary` drives the full pipeline: + +1. write the biomodel to VCML and round-trip through libvcell + (:func:`~pyvcell.vcml.utils.to_vcml_str`), which makes VCell compute the + geometry regions and auto-generate the canonical ``SpatialObjects``; +2. enable the surface's ``SurfaceVelocity`` quantity and inject a + ``SurfaceKinematics`` process carrying the front velocity declared by + :meth:`~pyvcell.vcml.models_app.Application.set_moving_boundary_front`; +3. round-trip again so libvcell generates the moving-boundary math + (a ``MembraneSubDomain`` with a velocity); +4. convert to a ``MovingBoundarySetup`` XML and run ``pyvcell_mbsolver``. +""" + +from __future__ import annotations + +import os +import tempfile +from pathlib import Path + +from lxml import etree +from lxml.etree import Element, _Element + +from pyvcell.sim_results.moving_boundary_result import MovingBoundaryResult +from pyvcell.vcml.models import Application, Biomodel, FrontVelocity, Simulation +from pyvcell.vcml.utils import to_vcml_str +from pyvcell.vcml.workspace import get_workspace_dir + +_VCML_NS = "http://sourceforge.net/projects/vcell/vcml" + + +def _q(tag: str) -> str: + return f"{{{_VCML_NS}}}{tag}" + + +def simulate_moving_boundary(biomodel: Biomodel, simulation: Simulation | str) -> MovingBoundaryResult: + """Run a moving-boundary simulation and return its moving-mesh trajectory. + + The named simulation must be configured for the Moving Boundary solver (see + :meth:`~pyvcell.vcml.models_app.Application.add_moving_boundary_sim`) and its + application must declare a moving front via + :meth:`~pyvcell.vcml.models_app.Application.set_moving_boundary_front`. + + Requires the ``native`` (libvcell >= 0.0.16) and ``mb`` (pyvcell-mbsolver) + extras. + """ + # Validate the model up front (cheap, no optional deps) so misconfiguration + # raises a clear error even when the solver extras are not installed. + simulation_name = simulation if isinstance(simulation, str) else simulation.name + application, sim = _find_simulation(biomodel, simulation_name) + if not sim.is_moving_boundary: + raise ValueError( + f"simulation '{simulation_name}' is not configured for the Moving Boundary solver " + f"(solver={sim.solver!r}); use Application.add_moving_boundary_sim(...)" + ) + if application.front_velocity is None: + raise ValueError( + f"application '{application.name}' has no moving front; " + f"call Application.set_moving_boundary_front(...) before simulating" + ) + + import libvcell + + try: + from pyvcell._internal.solvers.mbsolver import solve_moving_boundary + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + "pyvcell.vcml.simulate_moving_boundary requires the optional dependency 'pyvcell-mbsolver', " + "which is not installed. Install it with `pip install pyvcell[mb]`." + ) from exc + + # Step 1: round-trip so VCell computes regions and auto-generates SpatialObjects. + refined_vcml = to_vcml_str(biomodel) + + # Step 2: enable SurfaceVelocity and inject the SurfaceKinematics front velocity. + kinematics_vcml = _inject_front_kinematics(refined_vcml, application.name, application.front_velocity) + + out_dir = Path(tempfile.mkdtemp(prefix="mb_out_dir_", dir=get_workspace_dir())) + + # Step 3: round-trip again so libvcell generates the moving-boundary math. + refined_path = out_dir / "refined_mb.vcml" + success, message = libvcell.vcml_to_vcml(vcml_content=kinematics_vcml, vcml_file_path=refined_path) + if not success: + raise ValueError(f"Failed to generate moving-boundary math: {message}") + + # Step 4: convert to a MovingBoundarySetup XML. + success, message = libvcell.vcml_to_moving_boundary_input( + vcml_content=refined_path.read_text(), simulation_name=simulation_name, output_dir_path=out_dir + ) + if not success: + raise ValueError(f"Failed to generate moving-boundary solver input: {message}") + + setup_file = next((out_dir / f for f in os.listdir(out_dir) if f.endswith("mb.xml")), None) + if setup_file is None or setup_file.stat().st_size == 0: + raise ValueError("MovingBoundarySetup XML not found") + + species_names = _species_order(setup_file) + output_times = _output_times(sim) + return solve_moving_boundary(setup_file, species_names=species_names, output_times=output_times) + + +def _find_simulation(biomodel: Biomodel, simulation_name: str) -> tuple[Application, Simulation]: + for application in biomodel.applications: + for sim in application.simulations: + if sim.name == simulation_name: + return application, sim + raise ValueError(f"simulation '{simulation_name}' not found in biomodel '{biomodel.name}'") + + +def _inject_front_kinematics(vcml: str, application_name: str, front: FrontVelocity) -> str: + """Enable SurfaceVelocity on the front surface and add its SurfaceKinematics process.""" + root = etree.fromstring(vcml.encode()) + spec = next( + (s for s in root.iter(_q("SimulationSpec")) if s.get("Name") == application_name), + None, + ) + if spec is None: + raise ValueError(f"application '{application_name}' not found in regenerated VCML") + + surface_object = _select_surface_object(spec, front.surface_name) + surface_name = surface_object.get("Name") + if surface_name is None: + raise ValueError("regenerated surface object has no Name") + for category in surface_object.iter(_q("QuantityCategory")): + if category.get("Name") == "SurfaceVelocity": + category.set("Enabled", "true") + + processes = spec.find(_q("SpatialProcesses")) + if processes is None: + processes = Element("SpatialProcesses") + spec.append(processes) + process = Element("SpatialProcess", Name="sproc_0", Type="SurfaceKinematics", SurfaceObject=surface_name) + processes.append(process) + for parameter_name, role, value in ( + ("velocityX", "SurfaceVelocityX", front.velocity_x), + ("velocityY", "SurfaceVelocityY", front.velocity_y), + ("velocityZ", "SurfaceVelocityZ", front.velocity_z), + ): + parameter = Element("Parameter", Name=parameter_name, Role=role, Unit="um.s-1") + parameter.text = str(value) + process.append(parameter) + return etree.tostring(root).decode() + + +def _select_surface_object(spec: _Element, surface_name: str | None) -> _Element: + surface_objects = [obj for obj in spec.iter(_q("SpatialObject")) if obj.get("Type") == "Surface"] + if not surface_objects: + raise ValueError("regenerated geometry has no surface (need two subvolumes separated by a surface)") + if surface_name is None: + if len(surface_objects) > 1: + raise ValueError("geometry has multiple surfaces; specify surface_name in set_moving_boundary_front(...)") + return surface_objects[0] + # Match the requested surface class by the subvolumes it separates. + geometry = spec.find(_q("Geometry")) + refs: set[str] = set() + if geometry is not None: + for surface_class in geometry.iter(_q("SurfaceClass")): + if surface_class.get("Name") == surface_name: + refs = {surface_class.get("SubVolume1Ref", ""), surface_class.get("SubVolume2Ref", "")} + for obj in surface_objects: + if {obj.get("subVolumeInside", ""), obj.get("subVolumeOutside", "")} == refs: + return obj + raise ValueError(f"surface '{surface_name}' not found among regenerated spatial objects") + + +def _species_order(setup_file: Path) -> list[str]: + """Volume species in the order the solver reports concentrations.""" + root = etree.fromstring(setup_file.read_bytes()) + return [ + name + for species in root.iter("species") + if species.get("type") == "volume" and (name := species.get("name")) is not None + ] + + +def _output_times(sim: Simulation) -> list[float]: + step = sim.output_time_step + if step <= 0: + return [0.0, sim.duration] + count = round(sim.duration / step) + times = [min(i * step, sim.duration) for i in range(count + 1)] + if times[-1] < sim.duration: + times.append(sim.duration) + return times diff --git a/pyvcell/vcml/vcml_reader.py b/pyvcell/vcml/vcml_reader.py index 352cae53..3d24d296 100644 --- a/pyvcell/vcml/vcml_reader.py +++ b/pyvcell/vcml/vcml_reader.py @@ -236,14 +236,19 @@ def visit_Simulation(self, element: _Element, node: vc.Application) -> None: duration: float | None = None output_time_step: float | None = None mesh_size: tuple[int, int, int] | None = None + solver: str = vc.Simulation.model_fields["solver"].default + moving_boundary_options: vc.MovingBoundarySolverOptions | None = None for sim_child in element: if strip_namespace(sim_child.tag) == "SolverTaskDescription": solver_task_description_element = sim_child + solver = solver_task_description_element.get("Solver", default=solver) 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(child.tag) == "MovingBoundarySolverOptions": + moving_boundary_options = self._parse_moving_boundary_options(child) elif strip_namespace(sim_child.tag) == "MeshSpecification": mesh_specification_element = sim_child for mesh_child in mesh_specification_element: @@ -261,10 +266,24 @@ def visit_Simulation(self, element: _Element, node: vc.Application) -> None: duration=duration, output_time_step=output_time_step, mesh_size=mesh_size, + solver=solver, + moving_boundary_options=moving_boundary_options, version=self._parse_version(element), ) node.simulations.append(simulation) + @staticmethod + def _parse_moving_boundary_options(element: _Element) -> "vc.MovingBoundarySolverOptions": + defaults = vc.MovingBoundarySolverOptions() + values: dict[str, str] = {strip_namespace(child.tag): (child.text or "").strip() for child in element} + return vc.MovingBoundarySolverOptions( + front_to_node_ratio=float(values.get("FrontToNodeRatio", defaults.front_to_node_ratio)), + redistribution_mode=values.get("RedistributionMode") or defaults.redistribution_mode, + redistribution_version=values.get("RedistributionVersion") or defaults.redistribution_version, + redistribution_frequency=int(values.get("RedistributionFrequency", defaults.redistribution_frequency)), + extrapolation_method=values.get("ExtrapolationMethod") or defaults.extrapolation_method, + ) + def visit_MathDescription(self, element: _Element, node: vc.Application) -> None: name: str = element.get("Name", default="unnamed") math = vcm.MathDescription(name=name) diff --git a/pyvcell/vcml/vcml_writer.py b/pyvcell/vcml/vcml_writer.py index e51965bf..673f39c3 100644 --- a/pyvcell/vcml/vcml_writer.py +++ b/pyvcell/vcml/vcml_writer.py @@ -17,7 +17,7 @@ VCMLDocument, Version, ) -from pyvcell.vcml.models_app import AnnotatedFunction +from pyvcell.vcml.models_app import AnnotatedFunction, MovingBoundarySolverOptions from pyvcell.vcml.models_geometry import Geometry, SubVolumeType from pyvcell.vcml.models_math import ( CompartmentSubDomain, @@ -263,7 +263,7 @@ def write_application(self, application: Application, parent: _Element) -> None: "SolverTaskDescription", TaskType="Unsteady", UseSymbolicJacobian="false", - Solver="Sundials Stiff PDE Solver (Variable Time Step)", + Solver=simulation.solver, ) simulation_element.append(solver_task_description_element) solver_task_description_element.append( @@ -277,11 +277,14 @@ def write_application(self, application: Application, parent: _Element) -> None: 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) + if simulation.moving_boundary_options is not None: + self.write_moving_boundary_options(simulation.moving_boundary_options, solver_task_description_element) + else: + 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) @@ -295,6 +298,15 @@ def write_application(self, application: Application, parent: _Element) -> None: simulation_element.append(mesh_specification_element) self._write_version(simulation.version, simulation_element) + def write_moving_boundary_options(self, options: MovingBoundarySolverOptions, parent: _Element) -> None: + options_element = Element("MovingBoundarySolverOptions") + parent.append(options_element) + self._append_text_element(options_element, "FrontToNodeRatio", str(options.front_to_node_ratio)) + self._append_text_element(options_element, "RedistributionMode", options.redistribution_mode) + self._append_text_element(options_element, "RedistributionVersion", options.redistribution_version) + self._append_text_element(options_element, "RedistributionFrequency", str(options.redistribution_frequency)) + self._append_text_element(options_element, "ExtrapolationMethod", options.extrapolation_method) + def write_geometry(self, geometry: Geometry, parent: _Element) -> None: extent_element = Element( "Extent", X=str(geometry.extent[0]), Y=str(geometry.extent[1]), Z=str(geometry.extent[2]) diff --git a/tests/vcml/test_moving_boundary.py b/tests/vcml/test_moving_boundary.py new file mode 100644 index 00000000..3057c308 --- /dev/null +++ b/tests/vcml/test_moving_boundary.py @@ -0,0 +1,117 @@ +"""Moving-boundary solver support: authoring, VCML round-trip, and (gated) solve. + +The end-to-end test needs libvcell >= 0.0.16 (for ``vcml_to_moving_boundary_input``) +and the ``pyvcell-mbsolver`` package; it is skipped otherwise. The authoring and +writer/reader round-trip tests use only the lightweight data layer. +""" + +from __future__ import annotations + +import importlib.util + +import pytest + +import pyvcell.vcml as vc +from pyvcell.vcml.models import Biomodel, Model +from pyvcell.vcml.utils import load_vcml_str, to_vcml_str + + +def _mb_supported() -> bool: + if importlib.util.find_spec("pyvcell_mbsolver") is None: + return False + try: + import libvcell + + return hasattr(libvcell, "vcml_to_moving_boundary_input") + except Exception: + return False + + +def _moving_boundary_biomodel() -> Biomodel: + """A minimal valid moving-boundary model: a circle (``cell``) inside ``ec``.""" + geo = vc.Geometry(name="square", dim=2, extent=(10.0, 10.0, 10.0), origin=(0.0, 0.0, 0.0)) + geo.add_sphere("cell", radius=3.0, center=(5.0, 5.0, 0.0)) # inside region first (priority) + geo.add_background("ec") + geo.add_surface("cell_ec_membrane", "cell", "ec") + + model = Model(name="m") + model.add_compartment("cyt", dim=3) + model.add_compartment("ec", dim=3) + model.add_compartment("pm", dim=2) + model.add_species("C", "cyt") + + biomodel = Biomodel(name="MB", model=model) + app = biomodel.add_application("a", geometry=geo) + app.map_compartment("cyt", "cell") + app.map_compartment("ec", "ec") + app.map_compartment("pm", "cell_ec_membrane") + app.map_species("C", init_conc="x", diff_coef=10.0) + app.set_moving_boundary_front(velocity_x="sin(t)", velocity_y="cos(t)") + app.add_moving_boundary_sim(name="mbsim", duration=1.0, output_time_step=0.1, mesh_size=(31, 31, 1)) + return biomodel + + +def test_add_moving_boundary_sim_configures_solver() -> None: + biomodel = _moving_boundary_biomodel() + app = biomodel.applications[0] + sim = app.simulations[0] + + assert sim.solver == "MovingB" + assert sim.is_moving_boundary + assert sim.moving_boundary_options is not None + assert sim.moving_boundary_options.redistribution_mode == "FULL_REDIST" + assert app.front_velocity is not None + assert app.front_velocity.velocity_x == "sin(t)" + assert app.front_velocity.velocity_y == "cos(t)" + + +def test_moving_boundary_vcml_round_trip() -> None: + biomodel = _moving_boundary_biomodel() + # regenerate=False keeps this a pure data-layer round trip (no libvcell). + vcml = to_vcml_str(biomodel, regenerate=False) + assert 'Solver="MovingB"' in vcml + assert "MovingBoundarySolverOptions" in vcml + + reloaded = load_vcml_str(vcml) + sim = reloaded.applications[0].simulations[0] + assert sim.solver == "MovingB" + assert sim.moving_boundary_options is not None + assert sim.moving_boundary_options.redistribution_version == "EQUI_BOND_REDISTRIBUTE" + assert sim.moving_boundary_options.redistribution_frequency == 5 + + +def test_simulate_moving_boundary_rejects_non_moving_boundary_sim() -> None: + biomodel = _moving_boundary_biomodel() + app = biomodel.applications[0] + app.add_sim(name="fvsim", duration=1.0, output_time_step=0.1, mesh_size=(31, 31, 1)) + with pytest.raises(ValueError, match="not configured for the Moving Boundary solver"): + vc.simulate_moving_boundary(biomodel, "fvsim") + + +def test_simulate_moving_boundary_requires_front() -> None: + biomodel = _moving_boundary_biomodel() + biomodel.applications[0].front_velocity = None + with pytest.raises(ValueError, match="no moving front"): + vc.simulate_moving_boundary(biomodel, "mbsim") + + +@pytest.mark.skipif(not _mb_supported(), reason="requires libvcell>=0.0.16 and pyvcell-mbsolver") +def test_simulate_moving_boundary_end_to_end() -> None: + biomodel = _moving_boundary_biomodel() + result = vc.simulate_moving_boundary(biomodel, "mbsim") + + assert result.species_names == ["C"] + # output_time_step=0.1 over duration 1.0 -> 11 output frames. + assert len(result.frames) == 11 + assert result.times[0] == pytest.approx(0.0) + assert result.times[-1] == pytest.approx(1.0) + + first, last = result.frames[0], result.frames[-1] + # The moving front is a closed polygon of (x, y) points. + assert first.front.ndim == 2 and first.front.shape[1] == 2 + assert first.front.shape[0] > 0 + # Each inside element carries a concentration for every species. + assert first.concentrations["C"].shape == first.x.shape + assert first.x.shape[0] > 0 + # The front moves (prescribed sin/cos velocity), so its centroid shifts in time. + assert tuple(first.front.mean(axis=0)) != tuple(last.front.mean(axis=0)) diff --git a/uv.lock b/uv.lock index bef65baa..1c9455ff 100644 --- a/uv.lock +++ b/uv.lock @@ -3392,6 +3392,7 @@ all = [ { name = "python-dateutil" }, { name = "python-libsbml" }, { name = "pyvcell-fvsolver" }, + { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14'" }, { name = "pyvista" }, { name = "requests" }, { name = "requests-oauth2client" }, @@ -3418,6 +3419,10 @@ io = [ { name = "typer" }, { name = "zarr" }, ] +mb = [ + { name = "libvcell" }, + { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14'" }, +] native = [ { name = "libvcell" }, ] @@ -3481,8 +3486,10 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.10.5,<3" }, { name = "python-dateutil", marker = "extra == 'remote'", specifier = ">=2.8.2,<3" }, { name = "python-libsbml", marker = "extra == 'convert'", specifier = ">=5.20.4,<6" }, - { name = "pyvcell", extras = ["solver", "viz", "remote", "io", "convert", "native"], marker = "extra == 'all'" }, + { name = "pyvcell", extras = ["native"], marker = "extra == 'mb'" }, + { name = "pyvcell", extras = ["solver", "viz", "remote", "io", "convert", "native", "mb"], marker = "extra == 'all'" }, { name = "pyvcell-fvsolver", marker = "extra == 'solver'", specifier = ">=0.2.1" }, + { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14' and extra == 'mb'", specifier = ">=1.0.0,<2" }, { name = "pyvista", marker = "extra == 'viz'", specifier = ">=0.44.2,<1" }, { name = "requests", marker = "extra == 'remote'", specifier = ">=2.32.3,<3" }, { name = "requests-oauth2client", marker = "extra == 'remote'", specifier = ">=1.6.0,<2" }, @@ -3498,7 +3505,7 @@ requires-dist = [ { name = "vtk", marker = "extra == 'viz'", specifier = ">=9.3.1,<10" }, { name = "zarr", marker = "extra == 'io'", specifier = ">=2.17.2,<3" }, ] -provides-extras = ["solver", "viz", "remote", "io", "convert", "native", "all"] +provides-extras = ["solver", "viz", "remote", "io", "convert", "native", "mb", "all"] [package.metadata.requires-dev] dev = [ @@ -3546,6 +3553,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/cd/1a861a183c52da060d77d62f71971ed19e7e6add383903a316658108a4a6/pyvcell_fvsolver-0.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:03c8a84b92122c6c1f8d4d3fc2f0aac357cd6f6a0d7cf2a85a39d43480e4806e", size = 10972292, upload-time = "2026-06-10T17:39:25.897Z" }, ] +[[package]] +name = "pyvcell-mbsolver" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/c3/6b18d96bcd05913f5c4da5a4046d249c479efbd2eb245ed825cab2a74a1c/pyvcell_mbsolver-1.0.0.tar.gz", hash = "sha256:7a1ad025b924d6dba90e4a25989d060f60ae6be6faf7db8a42ec52d28f7d83bb", size = 1982079, upload-time = "2026-06-24T00:21:24.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/57/4b66e80153a68788ee35b9ce2e5bee5d4a59f34a92d56495b05b4ef17985/pyvcell_mbsolver-1.0.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:f6e8a82c055475ee4021a0b822a7e2e460d09c2fbb6bca0507c6cb7a546ee19b", size = 3009370, upload-time = "2026-06-24T00:21:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/46/5c/fd1bade319edcb3ba07bf0cdfeb5d647273e802f77feb6333b486b360319/pyvcell_mbsolver-1.0.0-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:65f5a41f41ccafa72aab3901f3595360134a55f8e85eb3ac0380e3e5a8190948", size = 3365731, upload-time = "2026-06-24T00:21:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/19/b4/f1de3603319847032b8290fa0bfebe9a614287f63bfc38f84bfa68e8be33/pyvcell_mbsolver-1.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7af987d44b35079162b50ff2d8ccd45da6c40dbfb4549d0d9e85350327d81c20", size = 3179247, upload-time = "2026-06-24T00:21:12.906Z" }, + { url = "https://files.pythonhosted.org/packages/72/f9/2dc128dd15ceb8dfa2f169d93d8c287450b602084e3fbd5a0faa723d6545/pyvcell_mbsolver-1.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4926079e9c3b7d3ad403f46327dd9f5039d61b5196b1890d7c21a904e7c84b43", size = 3427393, upload-time = "2026-06-24T00:21:14.925Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/ce0cbce26fc4bcec46e4bed52786984389d08750eb448e641fd17309044f/pyvcell_mbsolver-1.0.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:7d362feea354076925db5dde5995cb6ccd8253ee2450be5d7e8b9eb616e9e61a", size = 3009379, upload-time = "2026-06-24T00:21:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/da3da1f61a0111ab92f856768269c177b1102aa161971495875802f85f52/pyvcell_mbsolver-1.0.0-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:7baf922a9c0875dff9f81cbd4b80fd4fd244d71dc41d54290f34498403c2c535", size = 3365975, upload-time = "2026-06-24T00:21:18.857Z" }, + { url = "https://files.pythonhosted.org/packages/02/a1/04f71b0f10a4677460225dee437d13a361a802242232155842a4bafd9543/pyvcell_mbsolver-1.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8671d1bce48af578982e2e9d87344295ff1f83a95066bf11171764f3e3da2c59", size = 3178813, upload-time = "2026-06-24T00:21:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/0c/76/2a0fdaa267f8a90f1cfdd87bb02f03156ff0e12eaf72e7bc7b58886b7b60/pyvcell_mbsolver-1.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d22811b8ff6c077b6cb1eb4f284c865860e19ed136a130323d57ec632145e827", size = 3427190, upload-time = "2026-06-24T00:21:22.489Z" }, +] + [[package]] name = "pyvista" version = "0.48.4" From a863679b096ec8ed9661c374a117d2ee7577818c Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 24 Jun 2026 07:44:07 -0400 Subject: [PATCH 2/3] Require pyvcell-mbsolver >=1.0.1, drop py.typed workarounds pyvcell-mbsolver 1.0.1 ships a py.typed marker, so the package now type-checks natively. Bump the `mb` extra pin to >=1.0.1 and remove the workarounds that covered the untyped 1.0.0 wheel: - the [[tool.mypy.overrides]] for pyvcell_mbsolver - the `# type: ignore[misc]` on class _Collector(mb.SimulationObserver) Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 9 +-------- pyvcell/_internal/solvers/mbsolver.py | 2 +- uv.lock | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 96596e78..aa4e5c7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ native = [ # published to PyPI). pyvcell-mbsolver has no cp314 wheel yet, so it is gated to # Python < 3.14. mb = [ - "pyvcell-mbsolver>=1.0.0,<2 ; python_version < '3.14'", + "pyvcell-mbsolver>=1.0.1,<2 ; python_version < '3.14'", "pyvcell[native]", ] all = [ @@ -139,13 +139,6 @@ disable_error_code = [ "import-untyped" ] -[[tool.mypy.overrides]] -module = "pyvcell_mbsolver.*" -ignore_missing_imports = true -disable_error_code = [ - "import-untyped" -] - [[tool.mypy.overrides]] module = "trame.*" ignore_missing_imports = true diff --git a/pyvcell/_internal/solvers/mbsolver.py b/pyvcell/_internal/solvers/mbsolver.py index f87d35c6..9b9c7925 100644 --- a/pyvcell/_internal/solvers/mbsolver.py +++ b/pyvcell/_internal/solvers/mbsolver.py @@ -50,7 +50,7 @@ def solve_moving_boundary( targets = sorted(float(t) for t in output_times) if output_times is not None else None result = MovingBoundaryResult(species_names=species) - class _Collector(mb.SimulationObserver): # type: ignore[misc] + class _Collector(mb.SimulationObserver): def __init__(self) -> None: super().__init__() self._target_index = 0 diff --git a/uv.lock b/uv.lock index 1c9455ff..e6b2fc91 100644 --- a/uv.lock +++ b/uv.lock @@ -3489,7 +3489,7 @@ requires-dist = [ { name = "pyvcell", extras = ["native"], marker = "extra == 'mb'" }, { name = "pyvcell", extras = ["solver", "viz", "remote", "io", "convert", "native", "mb"], marker = "extra == 'all'" }, { name = "pyvcell-fvsolver", marker = "extra == 'solver'", specifier = ">=0.2.1" }, - { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14' and extra == 'mb'", specifier = ">=1.0.0,<2" }, + { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14' and extra == 'mb'", specifier = ">=1.0.1,<2" }, { name = "pyvista", marker = "extra == 'viz'", specifier = ">=0.44.2,<1" }, { name = "requests", marker = "extra == 'remote'", specifier = ">=2.32.3,<3" }, { name = "requests-oauth2client", marker = "extra == 'remote'", specifier = ">=1.6.0,<2" }, @@ -3555,18 +3555,18 @@ wheels = [ [[package]] name = "pyvcell-mbsolver" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/c3/6b18d96bcd05913f5c4da5a4046d249c479efbd2eb245ed825cab2a74a1c/pyvcell_mbsolver-1.0.0.tar.gz", hash = "sha256:7a1ad025b924d6dba90e4a25989d060f60ae6be6faf7db8a42ec52d28f7d83bb", size = 1982079, upload-time = "2026-06-24T00:21:24.591Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/57/4b66e80153a68788ee35b9ce2e5bee5d4a59f34a92d56495b05b4ef17985/pyvcell_mbsolver-1.0.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:f6e8a82c055475ee4021a0b822a7e2e460d09c2fbb6bca0507c6cb7a546ee19b", size = 3009370, upload-time = "2026-06-24T00:21:09.035Z" }, - { url = "https://files.pythonhosted.org/packages/46/5c/fd1bade319edcb3ba07bf0cdfeb5d647273e802f77feb6333b486b360319/pyvcell_mbsolver-1.0.0-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:65f5a41f41ccafa72aab3901f3595360134a55f8e85eb3ac0380e3e5a8190948", size = 3365731, upload-time = "2026-06-24T00:21:10.885Z" }, - { url = "https://files.pythonhosted.org/packages/19/b4/f1de3603319847032b8290fa0bfebe9a614287f63bfc38f84bfa68e8be33/pyvcell_mbsolver-1.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7af987d44b35079162b50ff2d8ccd45da6c40dbfb4549d0d9e85350327d81c20", size = 3179247, upload-time = "2026-06-24T00:21:12.906Z" }, - { url = "https://files.pythonhosted.org/packages/72/f9/2dc128dd15ceb8dfa2f169d93d8c287450b602084e3fbd5a0faa723d6545/pyvcell_mbsolver-1.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4926079e9c3b7d3ad403f46327dd9f5039d61b5196b1890d7c21a904e7c84b43", size = 3427393, upload-time = "2026-06-24T00:21:14.925Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/ce0cbce26fc4bcec46e4bed52786984389d08750eb448e641fd17309044f/pyvcell_mbsolver-1.0.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:7d362feea354076925db5dde5995cb6ccd8253ee2450be5d7e8b9eb616e9e61a", size = 3009379, upload-time = "2026-06-24T00:21:17.268Z" }, - { url = "https://files.pythonhosted.org/packages/06/54/da3da1f61a0111ab92f856768269c177b1102aa161971495875802f85f52/pyvcell_mbsolver-1.0.0-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:7baf922a9c0875dff9f81cbd4b80fd4fd244d71dc41d54290f34498403c2c535", size = 3365975, upload-time = "2026-06-24T00:21:18.857Z" }, - { url = "https://files.pythonhosted.org/packages/02/a1/04f71b0f10a4677460225dee437d13a361a802242232155842a4bafd9543/pyvcell_mbsolver-1.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8671d1bce48af578982e2e9d87344295ff1f83a95066bf11171764f3e3da2c59", size = 3178813, upload-time = "2026-06-24T00:21:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/0c/76/2a0fdaa267f8a90f1cfdd87bb02f03156ff0e12eaf72e7bc7b58886b7b60/pyvcell_mbsolver-1.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d22811b8ff6c077b6cb1eb4f284c865860e19ed136a130323d57ec632145e827", size = 3427190, upload-time = "2026-06-24T00:21:22.489Z" }, +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/8b/b52882872f1cdb5df94a9984e9316b5f22c4d29e848279295d6fd79a682d/pyvcell_mbsolver-1.0.1.tar.gz", hash = "sha256:cfb722d0cb4e736003112d8e66dba5b627cc3cbbafa253068bb8f6f10580f42a", size = 1982145, upload-time = "2026-06-24T06:52:49.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d9/266f47d82a17770678802cc4b00dde77b149dad6e07b6d8f3c69ea2655e5/pyvcell_mbsolver-1.0.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:33ddd4a6a58528bf2038b21f7ca6b4b492fb9d8c8ef07bb1e643e8c92805d073", size = 3009543, upload-time = "2026-06-24T06:52:35.243Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/442214439a9c241e75291cbde20df86b461125ae83b70074183eea83bf6d/pyvcell_mbsolver-1.0.1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:12c4838d6a2a617639e21d64a2f06ad43a82875ba204a68516717c28d1a4a807", size = 3365903, upload-time = "2026-06-24T06:52:36.966Z" }, + { url = "https://files.pythonhosted.org/packages/35/c8/515c4e60b7bae5da47229384e23a52a651296597880cc24b1fad254928d5/pyvcell_mbsolver-1.0.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26717c4f8156527e0a441733a33d2c75a17dd94db3729ae5562be734620e6086", size = 3179424, upload-time = "2026-06-24T06:52:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9e/0dd8fe691d47f632c2b2c599f087c1681f9c4ed7824ba189a67eb2f90d80/pyvcell_mbsolver-1.0.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8db3999fbac20d90ef5d8a8dc129907e6f11182d4cc1ac9eafb64ea8b9232d4", size = 3427562, upload-time = "2026-06-24T06:52:40.236Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/a1b5a2511ac0f5f11dd1c68ce449d384ddf374e078857858356cbb638208/pyvcell_mbsolver-1.0.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:256f2b8ffa55eb18f715128b72a5c97c17810ba90688af9e6fcd8a5204878e6e", size = 3009552, upload-time = "2026-06-24T06:52:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/96/2f/e1286f6b5260c7a508e0a709b31b679595101e72537f49737ba845664f02/pyvcell_mbsolver-1.0.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:edf09bde7bf9596abcaf38014fc3e74044b58f8437055d5f1e446bc2267cc86f", size = 3366149, upload-time = "2026-06-24T06:52:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/40/ff/ea4c5ba9488dc949917b9530695699149f664d04a212097300499c3738e5/pyvcell_mbsolver-1.0.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1589257d53940d32a2b6ecd3e50c78f9cfbfa07d7e48f0a74570da8722c7af30", size = 3178988, upload-time = "2026-06-24T06:52:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4b/41d9e459113564c300f7feabc21562b613d9810171d6b329747ac701d5a1/pyvcell_mbsolver-1.0.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbed09bce1fa650295ff8d336eae08c05b51e21d70aab9c5abc1ce67155d7784", size = 3427360, upload-time = "2026-06-24T06:52:47.502Z" }, ] [[package]] From f5c25c7dbb2e0f158862f402659cd1d4f4f3848c Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 24 Jun 2026 14:36:26 -0400 Subject: [PATCH 3/3] Bump to libvcell 0.0.17 and pyvcell-mbsolver 1.0.3; drop 3.14 gate Both moving-boundary dependencies now ship Python 3.14-compatible wheels: - libvcell 0.0.17 publishes universal py3-none- wheels (installable on every Python including 3.14) and includes moving-boundary support, so `native` requires >=0.0.17 and the code calls vcml_to_moving_boundary_input directly. - pyvcell-mbsolver 1.0.3 adds cp314 wheels and _core.pyi stubs, so the `mb` extra drops its temporary `python_version < '3.14'` gate and requires >=1.0.3. With both packages present and typed on all CI Python versions, the temporary mypy workarounds (pyvcell_mbsolver ignore_missing_imports + disallow_subclassing_any) are removed; mypy passes natively. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/getting-started/installation.md | 6 +-- pyproject.toml | 11 +++-- uv.lock | 63 +++++++++++++--------------- 3 files changed, 35 insertions(+), 45 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 0bb3292f..a45d2473 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -19,10 +19,8 @@ pip install "pyvcell[mb]" # moving-boundary solver (pyvcell-mbsolver) pip install "pyvcell[all]" # full feature set (solver, viz, remote, io, convert, native, mb) ``` -> The moving-boundary solver (`mb`) additionally needs `libvcell >= 0.0.16` to -> author its solver input; until that release is on PyPI, install `libvcell` -> 0.0.16 from a local wheel. `pyvcell-mbsolver` currently has no Python 3.14 -> wheel, so the `mb` extra is skipped on Python 3.14. +> The moving-boundary solver (`mb`) pulls `pyvcell-mbsolver` plus +> `libvcell >= 0.0.17` (the `native` extra) for moving-boundary input generation. ## Install for development (uv) diff --git a/pyproject.toml b/pyproject.toml index aa4e5c7e..a9b663e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,14 +59,13 @@ convert = [ "sympy>=1.13.1,<2", ] native = [ - "libvcell (>=0.0.15.3)", + # 0.0.17 ships universal py3-none wheels and includes moving-boundary support. + "libvcell (>=0.0.17)", ] -# Moving-boundary solver. Authoring moving-boundary input additionally requires -# libvcell >= 0.0.16 (the `native` extra is held at >=0.0.15.3 until 0.0.16 is -# published to PyPI). pyvcell-mbsolver has no cp314 wheel yet, so it is gated to -# Python < 3.14. +# Moving-boundary solver. libvcell (the `native` extra) generates the solver +# input; pyvcell-mbsolver runs it. mb = [ - "pyvcell-mbsolver>=1.0.1,<2 ; python_version < '3.14'", + "pyvcell-mbsolver>=1.0.3,<2", "pyvcell[native]", ] all = [ diff --git a/uv.lock b/uv.lock index e6b2fc91..44360fb5 100644 --- a/uv.lock +++ b/uv.lock @@ -1722,30 +1722,19 @@ wheels = [ [[package]] name = "libvcell" -version = "0.0.15.4" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f0/1e6e202c3fc8c516af3f503c0e6402d32d43808ac235a011e745e3956f8e/libvcell-0.0.15.4-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:e8e5e9cf2cda6a1076c7d2b0f7fe7ed006e4b305d427cd8ee56a9d6996a9b973", size = 40960357, upload-time = "2026-06-09T19:13:41.387Z" }, - { url = "https://files.pythonhosted.org/packages/b5/02/ef82cb6430106b5223d33f104f0cd46873204964f6816c0c48e06102c710/libvcell-0.0.15.4-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:7a656e1a6a60d027cefed3cae17a3aac4a9cf1dc3e5aa103c65bdb21d9d161d5", size = 41331064, upload-time = "2026-06-09T19:15:06.042Z" }, - { url = "https://files.pythonhosted.org/packages/cf/14/8b99a09135ed9fe42563469b11f670a0a6da5aeb7f8cfe5d135d0a69052f/libvcell-0.0.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d727d18864c0340196956f4f69f355c2762d3ea2cb1d8d160ca7290db04c763d", size = 42860940, upload-time = "2026-06-09T18:54:28.445Z" }, - { url = "https://files.pythonhosted.org/packages/ef/77/ab24ac04691aa07138c044b60d4baecdd1e27ae1b946ec7953e79a579e13/libvcell-0.0.15.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccdec300d716f0352c29e6b21ba1cea63b50040117af077b05285f07960cd370", size = 44147078, upload-time = "2026-06-09T18:52:41.485Z" }, - { url = "https://files.pythonhosted.org/packages/f9/68/fb27cdb8afa9445bbb4aeacf3305acf8fb8b0c88bb5827d3d79e967be7a2/libvcell-0.0.15.4-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:21b39d9c62abaebb2dccb35c1774049dc62acb2838e9559436f4b2bd2ce2ab21", size = 42888714, upload-time = "2026-06-09T18:57:13.243Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2e/ba208cdf6605b7f4ee5d7179fd38c583fd91d2c49a442093f03a265383ec/libvcell-0.0.15.4-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:15ebe87e98e791a0b01d52b55e964152b866c9f2dfe1b48dce838d324efc8c15", size = 44173083, upload-time = "2026-06-09T19:00:13.548Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/a52becf5428ee6466749c6baf410ae50b3a5435243cc257f2403908dcdd6/libvcell-0.0.15.4-cp312-cp312-win32.whl", hash = "sha256:cb219aa4d2ffb67059eabd80ff6a920a9fc6f651c5685478c72fb201b169ec6f", size = 41670426, upload-time = "2026-06-09T19:25:50.912Z" }, - { url = "https://files.pythonhosted.org/packages/da/de/c954512216eb03b8c5004f98a0831c8b21a6d51b548bbbe39d87ae572c3d/libvcell-0.0.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:ef8e6f18fb3ab7a3710817eaa1d47ad8ccf4fdcc9569ba427e91cee7d2e2d597", size = 41707383, upload-time = "2026-06-09T19:25:56.128Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5b/112715c89c9896b83f96c162a8ccc54d4905aca29f551ea3313c3d1817da/libvcell-0.0.15.4-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:708273ef3411d341c2e9142c6f03633e45a106c20cc0bdee074bb9b9da0303f7", size = 40930420, upload-time = "2026-06-09T19:13:44.189Z" }, - { url = "https://files.pythonhosted.org/packages/1c/49/325eae9fe5121fe8850e8bddc075d6799ca038419e7208d7d2fd51b9f80a/libvcell-0.0.15.4-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:b2695e96350b0ea65792a2a84602b0176ea69a8b419dc12a1ab52171c9fdb071", size = 41371490, upload-time = "2026-06-09T19:15:09.302Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/978fd3d6700d597a3dbf2a7a9d77b6f657743fcbddb224479047163eb1df/libvcell-0.0.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d422f96e4a1dab5a8f215cba4ff84e4ab5f12f7ce3c13a53b408d74b5efb05d4", size = 42879541, upload-time = "2026-06-09T18:54:32.543Z" }, - { url = "https://files.pythonhosted.org/packages/50/3e/3e5ef61361ea974d1e1af1c24ea4586817c02abc6ae317eadd56b772795b/libvcell-0.0.15.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19fcac7177f5f99863aab512861104138b5e43a0581e6a5cd65abc817014c11d", size = 44203212, upload-time = "2026-06-09T18:52:44.353Z" }, - { url = "https://files.pythonhosted.org/packages/a4/88/8bcdeb2c89677d603f89a5fb95564806fbbee0764fbe24b920346489f8d2/libvcell-0.0.15.4-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:e3c3325f95ef89400360e79abe1dad7bda0f57164c9ac04c972ba740332f71c6", size = 42890252, upload-time = "2026-06-09T18:57:17.343Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/ad54e65a04dfc13476ac278623dc22d63594244c735801a39213c028de8d/libvcell-0.0.15.4-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:715374bd733d52882ccc8d95ee22628491345188f34e3db470e8e7477b4a15e2", size = 44178190, upload-time = "2026-06-09T19:00:16.395Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6f/292cc929b32233896995a65a44ba90fc2f20b9448f88f4b2f146620cb079/libvcell-0.0.15.4-cp313-cp313-win32.whl", hash = "sha256:ce9d751d3c449d7a53755ad8d78fd689465eb40cf86483b0bd5762806ee2ab67", size = 41701183, upload-time = "2026-06-09T19:26:01.489Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/e86063435096287cd64475376a68f8f1655b20f4db7401d1eae2756b39e4/libvcell-0.0.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:ceb54c4e17c88df8cc14d8a359178765a45970e36632b3d8a6c67fa54d171d72", size = 41705212, upload-time = "2026-06-09T19:26:06.688Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2a/54d750d89f2aa9b9ab7dd5bdf8016af7a677a61ee42c0eed9531e17bae70/libvcell-0.0.15.4-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:ed19703739c3b04711f30724490be7910424718815b5afaab068e210496e6383", size = 42860835, upload-time = "2026-06-09T18:57:20.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/13/c569ac79a6ec2b6a333465161d8aced941d155d08cd8e7643de34dd3608a/libvcell-0.0.15.4-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:f317b2992351decd058636d4c02d3ac110dfddfc398185b45f3e1c112fdee434", size = 44227178, upload-time = "2026-06-09T19:00:19.529Z" }, + { url = "https://files.pythonhosted.org/packages/e6/20/d54aefdb51c1152f96c1c448c1ca91a06e38bfd471972362f4819360c1aa/libvcell-0.0.17-py3-none-macosx_13_0_arm64.whl", hash = "sha256:198ad9a4337f24d2b0632fe42e50ae5ee637389f279cff2f91d91cc8263786ed", size = 40970858, upload-time = "2026-06-24T15:48:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/14/35/2e0977be8acbf25b4c5aa278cedc8caeef21bc94065cf64f0caa150ea319/libvcell-0.0.17-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71a4bf9d856b2fec071afb09f9e9bb525e5467ec0d7d8dfea888f06226f9dda9", size = 42898613, upload-time = "2026-06-24T15:49:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/ac/40/d768c485fcfebb0b1c4e14045ddea95ed4a37448ca4f51618deab9dcba42/libvcell-0.0.17-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fae446eb7ca5fc65171e6f0d94dbecfc25d2a152e237b98f879bd4876b6e0a22", size = 44250033, upload-time = "2026-06-24T15:50:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/c395a3a5e3d11dcd382aa77c27120c40f306c78e5c159a94201b3ad61698/libvcell-0.0.17-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:70f0d37f05370b2548fc50b4f1685d2640607c5d2bf17af3f702aef52f21aaf9", size = 42915647, upload-time = "2026-06-24T15:49:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1e/299afe6195ecc14fd63dc674f6d90d252d51b2689050d01046fd777e3d22/libvcell-0.0.17-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:db20a7019361299d48b4bd159749d80f44c829b67e99784ac7294040ff931c53", size = 44244862, upload-time = "2026-06-24T15:50:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a2/71baa30871ad959edeaf57670a1faa48941d9ce1ff5606ac06414eb3c5b0/libvcell-0.0.17-py3-none-win32.whl", hash = "sha256:3422f681aa0b90dc1fa351117de6919d2171c9addcc554fd02b1baa35d9c64aa", size = 41688139, upload-time = "2026-06-24T15:49:47.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9c/d3accceb67a21f3c791ec947e736a1c24dbb0592921925b78962fbea05cc/libvcell-0.0.17-py3-none-win_amd64.whl", hash = "sha256:d69c16479cd32489d776bb6c1fa2cc8bc79eeb77974d26b3df58ebd393e15d59", size = 41771197, upload-time = "2026-06-24T15:49:57.011Z" }, ] [[package]] @@ -3392,7 +3381,7 @@ all = [ { name = "python-dateutil" }, { name = "python-libsbml" }, { name = "pyvcell-fvsolver" }, - { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14'" }, + { name = "pyvcell-mbsolver" }, { name = "pyvista" }, { name = "requests" }, { name = "requests-oauth2client" }, @@ -3421,7 +3410,7 @@ io = [ ] mb = [ { name = "libvcell" }, - { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14'" }, + { name = "pyvcell-mbsolver" }, ] native = [ { name = "libvcell" }, @@ -3476,7 +3465,7 @@ requires-dist = [ { name = "antimony", marker = "extra == 'convert'", specifier = ">=3.1.3" }, { name = "h5py", marker = "extra == 'io'", specifier = ">=3.11.0,<4" }, { name = "imageio", marker = "extra == 'viz'", specifier = ">=2.37.0,<3" }, - { name = "libvcell", marker = "extra == 'native'", specifier = ">=0.0.15.3" }, + { name = "libvcell", marker = "extra == 'native'", specifier = ">=0.0.17" }, { name = "lxml", specifier = ">=6.1.0,<7" }, { name = "matplotlib", marker = "extra == 'viz'", specifier = ">=3.10.0,<4" }, { name = "numexpr", specifier = ">=2.10,<3" }, @@ -3489,7 +3478,7 @@ requires-dist = [ { name = "pyvcell", extras = ["native"], marker = "extra == 'mb'" }, { name = "pyvcell", extras = ["solver", "viz", "remote", "io", "convert", "native", "mb"], marker = "extra == 'all'" }, { name = "pyvcell-fvsolver", marker = "extra == 'solver'", specifier = ">=0.2.1" }, - { name = "pyvcell-mbsolver", marker = "python_full_version < '3.14' and extra == 'mb'", specifier = ">=1.0.1,<2" }, + { name = "pyvcell-mbsolver", marker = "extra == 'mb'", specifier = ">=1.0.3,<2" }, { name = "pyvista", marker = "extra == 'viz'", specifier = ">=0.44.2,<1" }, { name = "requests", marker = "extra == 'remote'", specifier = ">=2.32.3,<3" }, { name = "requests-oauth2client", marker = "extra == 'remote'", specifier = ">=1.6.0,<2" }, @@ -3555,18 +3544,22 @@ wheels = [ [[package]] name = "pyvcell-mbsolver" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/8b/b52882872f1cdb5df94a9984e9316b5f22c4d29e848279295d6fd79a682d/pyvcell_mbsolver-1.0.1.tar.gz", hash = "sha256:cfb722d0cb4e736003112d8e66dba5b627cc3cbbafa253068bb8f6f10580f42a", size = 1982145, upload-time = "2026-06-24T06:52:49.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/d9/266f47d82a17770678802cc4b00dde77b149dad6e07b6d8f3c69ea2655e5/pyvcell_mbsolver-1.0.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:33ddd4a6a58528bf2038b21f7ca6b4b492fb9d8c8ef07bb1e643e8c92805d073", size = 3009543, upload-time = "2026-06-24T06:52:35.243Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5e/442214439a9c241e75291cbde20df86b461125ae83b70074183eea83bf6d/pyvcell_mbsolver-1.0.1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:12c4838d6a2a617639e21d64a2f06ad43a82875ba204a68516717c28d1a4a807", size = 3365903, upload-time = "2026-06-24T06:52:36.966Z" }, - { url = "https://files.pythonhosted.org/packages/35/c8/515c4e60b7bae5da47229384e23a52a651296597880cc24b1fad254928d5/pyvcell_mbsolver-1.0.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26717c4f8156527e0a441733a33d2c75a17dd94db3729ae5562be734620e6086", size = 3179424, upload-time = "2026-06-24T06:52:38.594Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9e/0dd8fe691d47f632c2b2c599f087c1681f9c4ed7824ba189a67eb2f90d80/pyvcell_mbsolver-1.0.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8db3999fbac20d90ef5d8a8dc129907e6f11182d4cc1ac9eafb64ea8b9232d4", size = 3427562, upload-time = "2026-06-24T06:52:40.236Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d1/a1b5a2511ac0f5f11dd1c68ce449d384ddf374e078857858356cbb638208/pyvcell_mbsolver-1.0.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:256f2b8ffa55eb18f715128b72a5c97c17810ba90688af9e6fcd8a5204878e6e", size = 3009552, upload-time = "2026-06-24T06:52:42.198Z" }, - { url = "https://files.pythonhosted.org/packages/96/2f/e1286f6b5260c7a508e0a709b31b679595101e72537f49737ba845664f02/pyvcell_mbsolver-1.0.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:edf09bde7bf9596abcaf38014fc3e74044b58f8437055d5f1e446bc2267cc86f", size = 3366149, upload-time = "2026-06-24T06:52:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/40/ff/ea4c5ba9488dc949917b9530695699149f664d04a212097300499c3738e5/pyvcell_mbsolver-1.0.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1589257d53940d32a2b6ecd3e50c78f9cfbfa07d7e48f0a74570da8722c7af30", size = 3178988, upload-time = "2026-06-24T06:52:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4b/41d9e459113564c300f7feabc21562b613d9810171d6b329747ac701d5a1/pyvcell_mbsolver-1.0.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbed09bce1fa650295ff8d336eae08c05b51e21d70aab9c5abc1ce67155d7784", size = 3427360, upload-time = "2026-06-24T06:52:47.502Z" }, +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/ef/84b92e371d0d74fb12812d217daca78d8fd67997a1eb57aab303298c7281/pyvcell_mbsolver-1.0.3.tar.gz", hash = "sha256:6380362de05db6caab61f32f6c327e09dc01082f9a53a873e0dbc1acc643e71a", size = 1983174, upload-time = "2026-06-24T18:27:39.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/db/a05afbf10fde22f56311f1c2f16583f3731e56dcf3016f2f477c7a580847/pyvcell_mbsolver-1.0.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e4fdb84792e746f6fe8163611bd3d4a1c0ea45f2c852db16d48945b3b034d04e", size = 3013155, upload-time = "2026-06-24T18:27:19.167Z" }, + { url = "https://files.pythonhosted.org/packages/51/2c/bfb9bd40ac32a6c0bc38116c87257aa791d9f1b16229d671d3d7d1e255fe/pyvcell_mbsolver-1.0.3-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:f6815005c470e6c0c7d4301e83f3eb779f55d218bd1e7109260f97b8eb38cc0f", size = 3369514, upload-time = "2026-06-24T18:27:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7c/379bd36383497d577b0444b37361b084c6eb66dcd5d01277adf764b85632/pyvcell_mbsolver-1.0.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0421fdf67502f518d4e26dae209bf91b4c790aaa25bba1df239d802232ea3fb", size = 3183037, upload-time = "2026-06-24T18:27:22.278Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/046f759ffe10dc76227fe905b1c2c65a66d3986854a4951836357a7c7fd6/pyvcell_mbsolver-1.0.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35e3c72a4a6211eb004b3cdf63e11096b5f039a0e8fe2b8ffb914f59f73dfb6d", size = 3431176, upload-time = "2026-06-24T18:27:23.926Z" }, + { url = "https://files.pythonhosted.org/packages/13/f9/fdd8d3e37a31ed8a51aeffacf3f30ef57ad4f8413eed2196a30f82e3cfa1/pyvcell_mbsolver-1.0.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a9313eadc78d5872fd658e35ba51274bcd27548856ea1e497d1b79e1ff7f4cf", size = 3013161, upload-time = "2026-06-24T18:27:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/94163e2fe3f221c69b6604340e80a381df67a45ac938b41d6b5c780a9e12/pyvcell_mbsolver-1.0.3-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:17dab4ecebbc80cb6ce4bb437f5da7abdb5fb6bc4e8c21ef3eaba76d1409f84e", size = 3369758, upload-time = "2026-06-24T18:27:27.09Z" }, + { url = "https://files.pythonhosted.org/packages/ea/69/2bf089a28daae0a2bf0bfc55f78a9729a635b9cde9803941482d9617a647/pyvcell_mbsolver-1.0.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99b715ae5eddb3b045eab834d205ecdec5b3bee6f94f15dfcb72e449bc20bc1d", size = 3182600, upload-time = "2026-06-24T18:27:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/5d276035c9c8fef4b7095ed2693c01bcf63d60dcc78cd2109fbe335a3a21/pyvcell_mbsolver-1.0.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fa6f46afcb1dbbf36713138da4981f5a7c2dc22e77c146f3b7fb090ef1261a9", size = 3430976, upload-time = "2026-06-24T18:27:30.261Z" }, + { url = "https://files.pythonhosted.org/packages/88/ac/59b0e3768c874b87551f74f073f3e1088807a96793b0ef7f0282669929cd/pyvcell_mbsolver-1.0.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:aa184b344b46b4adc2bf96d6b4b746cd0afc2812df36b318b62847c0866a7975", size = 3013497, upload-time = "2026-06-24T18:27:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/8d/45/f9ab1ceb7298ac902d77d34df6f0ccb4e729f02d30e41d63837808122bb9/pyvcell_mbsolver-1.0.3-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:d4ec998275c5f030afe70bea0107a3e199fd2d9d5018a9285621501f2dc0f63b", size = 3369794, upload-time = "2026-06-24T18:27:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/df/76/367f24868028fa5a5f95ed1662ca09eb470dfc0bdf676e7dd4ecc9428f69/pyvcell_mbsolver-1.0.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f1fa5d2f84515f6a26daaeb00ee7235d25edba241da899c213281a5b64833e7", size = 3183247, upload-time = "2026-06-24T18:27:35.994Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f8/6e7fff0ab23d49839e25ae6b28fe9fc3e0e5626ef23e1185a40422d291a3/pyvcell_mbsolver-1.0.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c1157f4c1835070768b3fd31def48ffeabce660e03d8d451b4103d79d249cd1", size = 3431249, upload-time = "2026-06-24T18:27:37.73Z" }, ] [[package]]