Fix UWexpression parameter display and tensor assignment - #49
Conversation
…ive models ExpressionDescriptor.__set__ was extracting the inner value from UWexpression assignments (value._sym), losing the symbolic reference. Parameters displayed as "1e21 Pa.s" instead of the symbolic name (η₀), and lazy evaluation broke. Now stores the UWexpression itself as a symbolic reference, preserving display, lazy evaluation, and the unit delegation chain. Two downstream issues were also fixed: - _unwrap_atom: recursively resolve UWQuantity when found inside UWexpression chain, preventing SympifyError during JIT compilation - _unscaled_matrix_to_rank4: wrap values with __getitem__ (UWexpression from MathematicalMixin) before NDimArray assignment, matching the existing guard in _build_c_tensor. This is the single choke point for all constitutive models after simplify() strips the per-model wrapping. Also fixed pre-existing bug in _unscaled_matrix_to_rank2 where the loop body never assigned values to the output matrix (always returned zeros). Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
This PR fixes critical issues with parameter handling and tensor assignment in Underworld3's symbolic expression system. It addresses bugs that prevented proper display of symbolic parameter names, broke lazy evaluation chains, and caused tensor conversion functions to fail or return incorrect values.
Changes:
- Simplified UWexpression parameter assignment to preserve symbolic references and lazy evaluation
- Fixed critical bug in
_unscaled_matrix_to_rank2where loop never assigned values (always returned zeros) - Added wrapping guard in
_unscaled_matrix_to_rank4to handle UWexpression objects during NDimArray assignment - Added recursive resolution in
_unwrap_atomto handle UWQuantity nested inside UWexpression chains
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/underworld3/utilities/_api_tools.py | Simplified ExpressionDescriptor.set to store UWexpression as symbolic reference instead of extracting inner value, preserving lazy evaluation and symbolic display |
| src/underworld3/maths/tensors.py | Fixed _unscaled_matrix_to_rank2 bug where loop body never assigned values; added getitem wrapping guard in _unscaled_matrix_to_rank4 for NDimArray assignment |
| src/underworld3/function/expressions.py | Added recursive UWQuantity resolution in _unwrap_atom to prevent SympifyError during JIT compilation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The function was removed during a cleanup but ckdtree.pyx (compiled Cython) still imports it. Added a thin delegate to units.get_units(). Underworld development team with AI support from Claude Code
Surface stores geometry internally in model (ND) space but user-facing properties (vertices, control_points) must return dimensional coordinates when the units system is active. Changes: - Add _dimensionalise_coords() gateway for Surface output properties - Add set_control_points() input gateway that non-dimensionalises - Fix _interpolate_to_proxy() to use internal ND coordinates for KDTree (was using the now-dimensional .vertices property, causing mismatch with mesh._coords) Underworld development team with AI support from Claude Code
|
Open review period ended, ready to merge. |
|
These are important bug fixes to the code rather than feature upgrades. This will be merged once the tests are fixed. |
|
CI failure is pre-existing and unrelated to this PR: 7 tests marked The Fatal Python abort is also pre-existing (PyVista/VTK cleanup in headless CI). All 280 actual tests pass. Merging. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| points = np.asarray(points.to_base_units().magnitude) / cs._length_scale | ||
| else: |
There was a problem hiding this comment.
In set_control_points(), the units-handling path assumes any unit-bearing input without .to_base_units() can safely use .magnitude directly. UnitAwareArray does not implement to_base_units(), so a UnitAwareArray in (for example) km would be treated as if it were already in base SI meters before dividing by cs._length_scale, producing incorrect internal model coordinates. Consider explicitly converting UnitAwareArray via its .to('meter') (or by constructing a Pint quantity with points.units and calling .to_base_units()) before stripping magnitude.
| points = np.asarray(points.to_base_units().magnitude) / cs._length_scale | |
| else: | |
| points = np.asarray(points.to_base_units().magnitude) / cs._length_scale | |
| elif hasattr(points, "to"): | |
| # Handle UnitAwareArray and similar types that provide .to() but | |
| # not .to_base_units(). Explicitly convert to meters first. | |
| points_in_m = points.to("meter") | |
| magnitude = getattr(points_in_m, "magnitude", points_in_m) | |
| points = np.asarray(magnitude) / cs._length_scale | |
| else: | |
| # Fallback: assume magnitude is already in base units |
| if coords is None: | ||
| coords = mesh.X.coords | ||
|
|
||
| if hasattr(coords, 'magnitude'): | ||
| coords = np.asarray(mesh._coords) | ||
| elif hasattr(coords, 'magnitude'): | ||
| coords = coords.magnitude | ||
| elif hasattr(coords, '__array__'): |
There was a problem hiding this comment.
SurfaceCollection.transfer_normals() now treats coords as model-space coordinates (matching mesh._coords / surface.face_centers). If callers pass unit-bearing coordinates (Pint Quantity / UnitAwareArray), the current logic strips .magnitude but does not convert from physical units back to model coordinates when the mesh coordinate system is scaled. This will query the KDTree in the wrong coordinate space when units/scaling are active. Consider mirroring set_control_points() by dividing by mesh.CoordinateSystem._length_scale (after converting to base SI) when _scaled is true.
These tests were marked xfail for bugs that have since been fixed (primarily by PR #49). Pytest treats unexpected passes (xpass) as failures in strict mode, causing CI to report failure despite all 280 tests actually passing. Tests promoted: - test_0720: RBF mesh variable state pollution resolved - test_0750: UnitAwareExpression .units, multiplication closure, get_units on compound expressions — all fixed - test_0753: UWexpression ND scaling now works - test_0757: UWexpression multiplication with coordinates fixed Underworld development team with AI support from Claude Code
Summary
1e21 Pa.s) instead of symbolic names (e.g.eta_0), and restores lazy evaluation.SympifyError.__getitem__wrapping guard for NDimArray assignment -- the single choke point for all constitutive models aftersimplify()strips per-model wrapping._interpolate_to_proxy()to use internal model-space coordinates for KDTree, avoiding mismatch when units system is active.Test plan
Underworld development team with AI support from Claude Code