diff --git a/src/underworld3/swarm.py b/src/underworld3/swarm.py index 5bf1dbe22..06e3ae35f 100644 --- a/src/underworld3/swarm.py +++ b/src/underworld3/swarm.py @@ -1065,6 +1065,12 @@ def _create_proxy_variable(self): continuous=self._proxy_continuous, varsymbol=r"\left<" + self.symbol + r"\right>", remesh_policy="reinit", + # The proxy is what `var.sym` resolves to, so it advertises + # the same units as the variable it stands for. Without this, + # evaluating a proxied symbol returned the NON-DIMENSIONAL + # number with no units attached, as though it were the answer + # (issue #439). Stored data stays non-dimensional either way. + units=self._units, ) # The remesh helper calls this on REINIT vars after an # adapt. Bound here so the closure captures ``self`` (the diff --git a/tests/test_0116_swarm_proxy_units.py b/tests/test_0116_swarm_proxy_units.py index 4eb08574e..050ea3716 100644 --- a/tests/test_0116_swarm_proxy_units.py +++ b/tests/test_0116_swarm_proxy_units.py @@ -105,3 +105,107 @@ def test_proxy_refresh_still_works_without_units(): del swarm del mesh + + +# --------------------------------------------------------------------------- +# The proxy advertises the swarm variable's units (issue #439). +# +# The proxy MeshVariable is what `var.sym` resolves to, so if it carries no +# units then reading a proxied variable through the symbolic path silently +# returns the NON-DIMENSIONAL number as though it were the answer. Measured +# before the fix, with density=3300 kg/m^3 as the reference and a stored +# value of 1.0: +# +# swarm.array 3300.0 proxy.array 1.0 <- disagree +# evaluate(rho.sym) -> ndarray 1.0, no units +# +# Stored data is non-dimensional on both sides either way; only what the +# proxy declares changes. +# --------------------------------------------------------------------------- +@pytest.fixture +def dimensional_model(): + orchestration_model = uw.get_default_model() + orchestration_model.set_reference_quantities( + length=uw.quantity(1000.0, "km"), + viscosity=uw.quantity(1e21, "Pa*s"), + density=uw.quantity(3300.0, "kg/m^3"), + ) + yield orchestration_model + uw.reset_default_model() + + +@pytest.fixture +def dimensional_mesh(dimensional_model): + return UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 8.0 + ) + + +def test_proxy_inherits_the_swarm_variable_units(dimensional_mesh): + swarm = uw.swarm.Swarm(dimensional_mesh) + var = swarm.add_variable(name="rho", size=1, proxy_degree=1, units="kg/m^3") + swarm.populate(fill_param=3) + + assert var._meshVar.units == var.units, ( + f"proxy advertises {var._meshVar.units}, variable advertises {var.units}" + ) + + del swarm + + +def test_proxy_and_swarm_dimensional_views_agree(dimensional_mesh): + """`.array` dimensionalises; both sides must reach the same number.""" + swarm = uw.swarm.Swarm(dimensional_mesh) + var = swarm.add_variable(name="rho", size=1, proxy_degree=1, units="kg/m^3") + swarm.populate(fill_param=3) + + var.data[:, 0] = 1.0 # non-dimensional 1.0 == 3300 kg/m^3 + var._update_proxy_if_stale() + + swarm_value = np.asarray(var.array).ravel()[0] + proxy_value = np.asarray(var._meshVar.array).ravel()[0] + + assert np.isclose(swarm_value, 3300.0, rtol=1e-10) + assert np.isclose(proxy_value, swarm_value, rtol=1e-10), ( + f"swarm .array {swarm_value:.6g} but proxy .array {proxy_value:.6g}" + ) + # Storage stays non-dimensional on both sides. + assert np.isclose(np.asarray(var._meshVar.data[0, 0]), 1.0, rtol=1e-10) + + del swarm + + +def test_evaluating_a_proxied_symbol_returns_dimensional_values(dimensional_mesh): + """The silent-wrong-value case: evaluate used to return the raw 1.0.""" + from underworld3.utilities.unit_aware_array import UnitAwareArray + + swarm = uw.swarm.Swarm(dimensional_mesh) + var = swarm.add_variable(name="rho", size=1, proxy_degree=1, units="kg/m^3") + swarm.populate(fill_param=3) + + var.data[:, 0] = 1.0 + values = uw.function.evaluate(var.sym[0], np.array([[0.5, 0.5], [0.25, 0.75]])) + + assert isinstance(values, UnitAwareArray), ( + f"evaluate returned {type(values).__name__}; a units-carrying proxy " + "must not present non-dimensional numbers as the answer" + ) + assert str(values.units) == "kilogram / meter ** 3" + assert np.allclose(np.asarray(values).ravel(), 3300.0, rtol=1e-8) + + del swarm + + +def test_variable_without_units_keeps_a_dimensionless_proxy(dimensional_mesh): + """Only variables that declare units get a units-carrying proxy.""" + swarm = uw.swarm.Swarm(dimensional_mesh) + var = swarm.add_variable(name="f", size=1, proxy_degree=1) + swarm.populate(fill_param=3) + + assert var._meshVar.units is None + + var.data[:, 0] = 1.0 + var._update_proxy_if_stale() + assert np.isclose(np.asarray(var._meshVar.array).ravel()[0], 1.0, rtol=1e-10) + + del swarm