MathematicalMixin Symbolic Behavior Design Document¶
Date: 2025-10-26 Author: Claude Status: Design Phase
Executive Summary¶
The MathematicalMixin class currently causes premature numeric substitution in arithmetic operations, breaking lazy evaluation and symbolic expression display. This document provides a comprehensive analysis and unified solution that preserves symbolic behavior across all classes while maintaining backward compatibility.
Problem Statement¶
Current Behavior (BROKEN)¶
Ra = uw.expression(r"\mathrm{Ra}", 72435330.0)
T = uw.MeshVariable("T", mesh, 1)
# Current: Ra * T → 72435330.0 * T (numeric substitution)
# Expected: Ra * T → Ra * T (symbolic preservation)
Root Cause¶
In mathematical_mixin.py lines 256-258:
def __mul__(self, other):
# Convert MathematicalMixin arguments to their symbolic form
if hasattr(other, "_sympify_"):
other = other.sym # BUG: Substitutes numeric value immediately!
This pattern appears in ALL arithmetic operations: __add__, __sub__, __mul__, __truediv__, __pow__, and their right-hand equivalents.
Affected Classes¶
1. SwarmVariable¶
Inheritance:
DimensionalityMixin, MathematicalMixin, Stateful, uw_objectPurpose: Particle data with optional mesh proxy variables
Symbolic Behavior: Should remain symbolic in expressions
Units: Inherits from DimensionalityMixin for unit tracking
Unwrapping: Via
unwrap()when evaluating expressions
2. EnhancedMeshVariable (exported as MeshVariable)¶
Inheritance:
DimensionalityMixin, UnitAwareMixin, MathematicalMixinPurpose: Mesh-based field variables
Symbolic Behavior: Must remain symbolic for solver expressions
Units: Full unit support via UnitAwareMixin
Unwrapping: Critical for solver compilation
3. UWexpression¶
Inheritance:
MathematicalMixin, UWQuantity, uw_object, SymbolPurpose: Named constants and parameters with LaTeX display
Symbolic Behavior: MUST remain symbolic (inherits from Symbol!)
Units: Full UWQuantity unit support
Unwrapping: Substitutes numeric value during compilation
Special: Already overrides arithmetic operations to preserve symbolism
Design Principles¶
1. Lazy Evaluation (CRITICAL)¶
Expressions must remain symbolic until explicitly unwrapped for compilation:
# Building expressions (symbolic)
momentum = density * velocity # Should be ρ * v, not numeric
strain_rate = velocity.diff(x) # Should be ∂v/∂x, not evaluated
# Unwrapping for compilation (numeric substitution)
compiled = unwrap(momentum) # NOW substitute values
2. Symbolic Display¶
Users expect to see meaningful symbolic representations:
Ra * T # Should display as "Ra * T" for pedagogical clarity
# Not "72435330.0 * T" which obscures meaning
3. Unit Propagation¶
Units must flow through arithmetic operations:
L0 = uw.quantity(2900, "km")
L0**3 # Must yield 'kilometer ** 3' units
4. Backward Compatibility¶
Existing code using .sym directly must continue working.
Proposed Solution¶
Core Fix: Conditional Substitution¶
Strategy: Only substitute .sym for non-MathematicalMixin objects.
class MathematicalMixin:
def __mul__(self, other):
"""Multiplication preserving symbolic expressions."""
sym = self._validate_sym()
# KEY CHANGE: Don't substitute .sym for MathematicalMixin objects
if hasattr(other, "_sympify_") and not isinstance(other, MathematicalMixin):
other = other._sympify_() # Use protocol, not .sym directly
try:
return sym * other
except (TypeError, ValueError) as e:
raise TypeError(f"Cannot multiply {type(self).__name__} and {type(other).__name__}: {e}")
Why This Works¶
MathematicalMixin * MathematicalMixin: Preserves both as symbols
MathematicalMixin * SymPy: Works via SymPy’s dispatch
MathematicalMixin * Number: Works via SymPy’s handling
MathematicalMixin * Other: Calls
_sympify_()protocol
Apply to All Operations¶
This pattern must be applied to:
__add__,__radd____sub__,__rsub____mul__,__rmul____truediv__,__rtruediv____pow__,__rpow__
Class-Specific Behaviors¶
SwarmVariable & MeshVariable¶
# Current (broken)
velocity * pressure # → velocity.sym * pressure.sym (numeric arrays!)
# Fixed
velocity * pressure # → V * P (symbolic Functions)
# Unwrapping
unwrap(velocity * pressure) # → Substitutes actual values
UWexpression¶
# Already has overrides to preserve symbolic behavior
def __mul__(self, other):
return sympy.Symbol.__mul__(self, other) # Uses Symbol's method
# This is correct! But MathematicalMixin breaks it from the other side
Mixed Operations¶
# Expression * Variable
Ra * T # → Ra * T (both remain symbolic)
# Expression * Expression
Ra * alpha # → Ra * α (symbolic product)
# Variable * Variable
velocity * density # → v * ρ (symbolic)
Implementation Plan¶
Phase 1: Fix MathematicalMixin (PRIORITY)¶
Update all arithmetic operations to use conditional substitution
Preserve
_sympify_()protocol usageAdd comprehensive tests for symbolic preservation
Phase 2: Remove UWexpression Overrides¶
Once MathematicalMixin is fixed, remove redundant overrides
Simplify inheritance chain
Verify all operations still work
Phase 3: Validate Unwrapping¶
Ensure
unwrap()correctly substitutes all valuesTest with complex nested expressions
Verify solver compilation still works
Phase 4: Unit Integration¶
Ensure units propagate through symbolic operations
Test dimensional analysis with symbolic expressions
Verify unit cancellation in unwrapped expressions
Test Cases¶
1. Symbolic Preservation¶
def test_symbolic_preservation():
Ra = uw.expression("Ra", 1e7)
T = uw.MeshVariable("T", mesh, 1)
expr = Ra * T
# Should contain Ra symbol, not 1e7
assert Ra in expr.atoms()
assert 1e7 not in expr.atoms()
2. Lazy Evaluation¶
def test_lazy_evaluation():
Ra = uw.expression("Ra", 1e7)
T = uw.MeshVariable("T", mesh, 1)
expr = Ra * T
Ra.sym = 2e7 # Change value
unwrapped = unwrap(expr)
# Should use new value 2e7
assert 2e7 in str(unwrapped)
3. Unit Propagation¶
def test_unit_propagation():
L0 = uw.quantity(100, "km")
v = uw.MeshVariable("v", mesh, 1, units="m/s")
expr = v * L0
units = uw.get_units(expr)
assert units == "meter * kilometer / second"
Migration Strategy¶
Backward Compatibility¶
All existing
.symusage continues workingunwrap()function remains unchangedSolver interfaces unchanged
Gradual Rollout¶
Week 1: Implement and test MathematicalMixin fix
Week 2: Remove UWexpression overrides if safe
Week 3: Comprehensive testing with notebooks
Week 4: Documentation and user guide updates
Risk Mitigation¶
Keep old behavior behind feature flag initially
Extensive test coverage before deployment
Monitor solver performance (no changes expected)
Alternative Approaches Considered¶
1. Always Use SymPy Protocol (Rejected)¶
# Always return self from _sympify_()
def _sympify_(self):
return self # Not self.sym
Problem: Causes recursion in operations like atoms()
2. Separate Symbolic and Numeric Mixins (Rejected)¶
class SymbolicMixin: ... # For expressions
class NumericMixin: ... # For variables
Problem: Too much code duplication, breaks existing inheritance
3. Wrapper Objects (Rejected)¶
class SymbolicWrapper:
def __init__(self, obj): ...
Problem: Adds complexity, breaks isinstance() checks
Conclusion¶
The proposed solution of conditional substitution in MathematicalMixin arithmetic operations:
Preserves symbolic behavior for all MathematicalMixin objects
Maintains backward compatibility with existing code
Enables proper lazy evaluation for expression changes
Supports unit propagation through symbolic operations
Requires minimal code changes (only in MathematicalMixin)
This approach aligns with the original design intent where expressions remain symbolic as long as possible, with unwrap() providing explicit control over when numeric substitution occurs.
Appendix: Detailed Code Analysis¶
Current Call Flow (Broken)¶
Ra * T
├─> MathematicalMixin.__mul__(Ra, T)
│ ├─> if hasattr(T, "_sympify_"): T = T.sym # BUG!
│ └─> return Ra.sym * T.sym # Returns 1e7 * T.sym
Fixed Call Flow¶
Ra * T
├─> MathematicalMixin.__mul__(Ra, T)
│ ├─> if isinstance(T, MathematicalMixin): keep T as-is
│ └─> return Ra.sym * T # Returns Ra_symbol * T_symbol
Unwrap Flow (Unchanged)¶
unwrap(Ra * T)
├─> Extract atoms: {Ra, T}
├─> Substitute: Ra → 1e7, T → T.sym
└─> Return: 1e7 * T.sym # Numeric substitution happens here