diff --git a/pyvcell/vcml/vcml_writer.py b/pyvcell/vcml/vcml_writer.py index 673f39c..4423620 100644 --- a/pyvcell/vcml/vcml_writer.py +++ b/pyvcell/vcml/vcml_writer.py @@ -410,19 +410,17 @@ def write_species_mapping(self, mapping: SpeciesMapping, parent: _Element) -> No elif boundary_value_count > 0: raise ValueError(f"SpeciesMapping {mapping.species_name} has {boundary_value_count} boundary values") if mapping.velocity_x is not None or mapping.velocity_y is not None or mapping.velocity_z is not None: + # Emit every component that is set, INCLUDING a literal "0.0". Dropping zero components broke the + # Moving Boundary solver, which reads the VCML directly and requires each needed component to be + # present (an axis-aligned advection like velocity=(v, 0) lost its Y and failed with "VelocityY is + # null"). The FV reader defaults a missing component to 0, so emitting "0.0" stays round-trip-faithful. velocity_element = Element("Velocity") if mapping.velocity_x is not None: - str_val_x = str(mapping.velocity_x) - if str_val_x != "0.0": - velocity_element.set("X", str_val_x) + velocity_element.set("X", str(mapping.velocity_x)) if mapping.velocity_y is not None: - str_val_y = str(mapping.velocity_y) - if str_val_y != "0.0": - velocity_element.set("Y", str_val_y) + velocity_element.set("Y", str(mapping.velocity_y)) if mapping.velocity_z is not None: - str_val_z = str(mapping.velocity_z) - if str_val_z != "0.0": - velocity_element.set("Z", str_val_z) + velocity_element.set("Z", str(mapping.velocity_z)) parent.append(velocity_element) @staticmethod diff --git a/tests/vcml/test_moving_boundary.py b/tests/vcml/test_moving_boundary.py index 3057c30..8b18028 100644 --- a/tests/vcml/test_moving_boundary.py +++ b/tests/vcml/test_moving_boundary.py @@ -80,6 +80,23 @@ def test_moving_boundary_vcml_round_trip() -> None: assert sim.moving_boundary_options.redistribution_frequency == 5 +def test_species_advection_velocity_emits_zero_components() -> None: + # Regression: the writer dropped a velocity component equal to "0.0", so an axis-aligned species + # advection v = (vx, 0) lost its Y attribute and the Moving Boundary solver (which requires each + # component) failed with "VelocityY is null". The component must be emitted, and round-trip. + biomodel = _moving_boundary_biomodel() + sm = biomodel.applications[0].get_species_mapping("C") + sm.velocity_x = "0.5" + sm.velocity_y = "0.0" # the dropped-on-write case + + vcml = to_vcml_str(biomodel, regenerate=False) + assert 'X="0.5"' in vcml and 'Y="0.0"' in vcml # both present (Y no longer dropped) + + reloaded_sm = load_vcml_str(vcml).applications[0].get_species_mapping("C") + assert reloaded_sm.velocity_x is not None and float(reloaded_sm.velocity_x) == 0.5 + assert reloaded_sm.velocity_y is not None and float(reloaded_sm.velocity_y) == 0.0 + + def test_simulate_moving_boundary_rejects_non_moving_boundary_sim() -> None: biomodel = _moving_boundary_biomodel() app = biomodel.applications[0]