Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions pyvcell/vcml/vcml_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions tests/vcml/test_moving_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading