Conversation
An expression's identity is its NAME: uw.expression(r"\eta", ...) returns the
existing container so a formula written early keeps seeing later edits. But
__init__ then ran on that returned object and overwrote its contents and its
description from the arguments - so a line that reads like a declaration
silently changed every formula already written against the name.
Re-declaration now:
* with the SAME value, passes. Declaring the same thing twice changes nothing
and no formula can tell; a factory that rebuilds an unmutated problem in one
process is doing exactly that, and refusing it would be noise.
* with a DIFFERENT value, raises, naming the two things the caller might have
meant: `name.sym = value` to change the contents, uw.expression(name) to
fetch. The existing contents are left untouched.
Anything that cannot be compared counts as different, so the loud path is the
default: a container whose contents we cannot reason about is the last one to
overwrite quietly.
Blast radius, measured before choosing rather than after. Surveying 576 tests
(bands 00-03) recorded 15 re-declarations in total: 6 with the same value and 9
with a different one, across five names - and NO library code among them. All
nine were test factories that rebuild the same named problem in one process,
where the silent overwrite was load-bearing. The most instructive is
\eta_{w302}, whose container still held `2.0*k_n302 + 0.05*{T302}(N.x, N.y)**2`
from the FIRST build: the overwrite is what rebound it to the second build's
mesh variable, and nothing said so.
Those three factories now declare-or-assign through a small local helper, which
makes the rebinding the visible act it always was.
tests/test_00*|01*|02*|03*py: 576 passed.
Underworld development team with AI support from Claude Code
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #775 — merge that first, then this retargets to
development. Split out of #775 because it is a behaviour change users will notice, and it deserves its own review rather than riding along with a snapshot fix.What was happening
An expression's identity is its name:
uw.expression(r"\eta", ...)returns the existing container, which is what lets a formula written early keep seeing later edits to its contents. That much is deliberate.But
__init__then ran on that returned object and overwrote its contents and its description from the arguments. So a line that reads like a declaration silently changed every formula already written against the name:The rule now
name.sym = valueto change contents,uw.expression(name)to fetch). The existing contents are left untouched.Blast radius, measured before choosing
Surveyed 576 tests (bands 00–03) with the refusal instrumented to record rather than raise:
All nine were test factories rebuilding the same named problem in one process, where the silent overwrite was load-bearing.
The most instructive is
\eta_{w302}, whose container still heldfrom the first build — referencing the first build's mesh variable. The overwrite is what rebound it to the second build's variable, and nothing said so. That is the hazard in one line: not a stale value, a stale reference, invisible.
Those three factories now declare-or-assign through a small local helper, which makes the rebinding the visible act it always was:
Tests
tests/test_00*|01*|02*|03*py: 576 passed. Two contract tests added — a name fetched twice is one container captured once, and re-declaring with a value raises while leaving the contents untouched.Still open, not addressed here
_expr_namesis a class attribute, so it outlives the model: afterreset_default_model()a fresh model re-uses containers from the previous one, carrying their values until something sets them. Separate decision — clear the registry on reset, or namespace it per model.Underworld development team with AI support from Claude Code