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
6 changes: 6 additions & 0 deletions src/underworld3/swarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions tests/test_0116_swarm_proxy_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading