From 5e6e495bba69b0bcbffc058d5df5f2f368664c27 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 6 Jul 2026 16:49:58 +0100 Subject: [PATCH 1/5] refactor(internal): WB-01 migrate swarm.py off deprecated access() context managers Remove the four remaining live 'with ...access(...)' wrappers in swarm.py (SWARM-13, Wave B of the 2026-07 remediation worklist): - SwarmVariable._rbf_reduce_to_meshVar: read-only block, wrapper dropped - NodalSwarm.__init__ (nX0/nI0 launch-point writes) - NodalSwarm.advection (_X0 / _nR0 writes) Swarm.access() is already a no-op compatibility shim whose only effect is to defer NDArray_With_Callback callbacks to context exit; direct .data writes fire the identical canonical PETSc-pack callback immediately, so the end state is unchanged (the sanctioned raw .data exception, Charter section 7). None of the written variables is the swarm coordinate variable, so no migration semantics are involved. The audit counted 13 sites at development@1d003481; Track 0 (#310/#313), Wave A, and Wave C had already removed all but these four. Underworld development team with AI support from Claude Code --- src/underworld3/swarm.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/underworld3/swarm.py b/src/underworld3/swarm.py index c648f01de..cba131666 100644 --- a/src/underworld3/swarm.py +++ b/src/underworld3/swarm.py @@ -1230,19 +1230,18 @@ def _rbf_reduce_to_meshVar(self, meshVar, verbose=False): # Use cached KDTree for interpolation (avoids redundant index construction) kd = meshVar._get_kdtree() - with self.swarm.access(): - d, n = kd.query(self.swarm.data, k=1, sqr_dists=False) # need actual distances + d, n = kd.query(self.swarm.data, k=1, sqr_dists=False) # need actual distances - node_values = np.zeros((meshVar.coords.shape[0], self.num_components)) - w = np.zeros(meshVar.coords.shape[0]) + node_values = np.zeros((meshVar.coords.shape[0], self.num_components)) + w = np.zeros(meshVar.coords.shape[0]) - if not self._nn_proxy: - for i in range(self.local_size): - # if b[i]: - node_values[n[i], :] += self.data[i, :] / (1.0e-24 + d[i]) - w[n[i]] += 1.0 / (1.0e-24 + d[i]) + if not self._nn_proxy: + for i in range(self.local_size): + # if b[i]: + node_values[n[i], :] += self.data[i, :] / (1.0e-24 + d[i]) + w[n[i]] += 1.0 / (1.0e-24 + d[i]) - node_values[np.where(w > 0.0)[0], :] /= w[np.where(w > 0.0)[0]].reshape(-1, 1) + node_values[np.where(w > 0.0)[0], :] /= w[np.where(w > 0.0)[0]].reshape(-1, 1) # 2 - set NN vals on mesh var where w == 0.0 @@ -5176,9 +5175,8 @@ def __init__( nswarm.dm.migrate(remove_sent_points=True) - with nswarm.access(nX0, nI0): - nX0.data[:, :] = coords - nI0.data[:, 0] = range(0, coords.shape[0]) + nX0.data[:, :] = coords + nI0.data[:, 0] = range(0, coords.shape[0]) self._nswarm = nswarm self._nX0 = nX0 @@ -5199,11 +5197,9 @@ def advection( step_limit=True, ): - with self.access(self._X0): - self._X0.data[...] = self._nX0.data[...] + self._X0.data[...] = self._nX0.data[...] - with self.access(self._nR0): - self._nR0.data[...] = uw.mpi.rank + self._nR0.data[...] = uw.mpi.rank super().advection( V_fn, From a4c1e51fb2a9453d642bb388306af8ee779aef67 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 6 Jul 2026 16:50:08 +0100 Subject: [PATCH 2/5] refactor(internal): WB-03 migrate adaptivity.py off deprecated access() context managers Remove the four 'with mesh.access(...)' / 'with swarm.access(...)' wrappers in adaptivity.py (Wave B of the 2026-07 remediation worklist): - create_metric: direct metric.data write - metric_from_field: read-only indicator.data copy - mesh2mesh_swarm: read-only particle-coordinate/variable gather - mesh2mesh_meshVariable: direct tmp_varS.data write Both access() shims are no-op compatibility wrappers that only defer NDArray_With_Callback callbacks to context exit; direct .data access is the sanctioned raw-copy exception (Charter section 7) and produces the identical PETSc pack. Underworld development team with AI support from Claude Code --- src/underworld3/adaptivity.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/underworld3/adaptivity.py b/src/underworld3/adaptivity.py index 5442960d0..c22a1fb8b 100644 --- a/src/underworld3/adaptivity.py +++ b/src/underworld3/adaptivity.py @@ -148,10 +148,9 @@ def create_metric( # Create metric MeshVariable metric = uw.discretisation.MeshVariable(name, mesh, 1, degree=1) - with mesh.access(metric): - # Convert to metric tensor: M = 1/h² × I (isotropic) - # This is dimension-independent: same formula for 2D and 3D - metric.data[:, 0] = 1.0 / (h_values ** 2) + # Convert to metric tensor: M = 1/h² × I (isotropic) + # This is dimension-independent: same formula for 2D and 3D + metric.data[:, 0] = 1.0 / (h_values ** 2) return metric @@ -402,8 +401,7 @@ def metric_from_field( ) # Get indicator values - with mesh.access(indicator): - ind_values = indicator.data[:, 0].copy() + ind_values = indicator.data[:, 0].copy() # Handle indicator bounds if indicator_min is None: @@ -647,10 +645,9 @@ def mesh2mesh_swarm(mesh0, mesh1, swarm0, swarmVarList, proxy=True, verbose=Fals if the returned swarm is ephemeral """ - with swarm0.access(): - swarm_data = swarm0._particle_coordinates.data.copy() - for swarmVar in swarmVarList: - swarm_data = np.hstack((swarm_data, np.ascontiguousarray(swarmVar.data.astype(float)))) + swarm_data = swarm0._particle_coordinates.data.copy() + for swarmVar in swarmVarList: + swarm_data = np.hstack((swarm_data, np.ascontiguousarray(swarmVar.data.astype(float)))) s_coords0 = np.ascontiguousarray(swarm_data[:, 0 : mesh0.dim]) @@ -891,8 +888,7 @@ def mesh2mesh_meshVariable(meshVar0, meshVar1, verbose=False): # print(f"Map data to swarm (rbf) - points = {tmp_swarm.dm.getSize()}", flush=True) - with tmp_swarm.access(tmp_varS): - tmp_varS.data[...] = meshVar0.rbf_interpolate(tmp_swarm._particle_coordinates.data) + tmp_varS.data[...] = meshVar0.rbf_interpolate(tmp_swarm._particle_coordinates.data) # print(f"Distribute swarm", flush=True) From 06b91bbe322b10094027b190c64d8fea7262dc91 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 6 Jul 2026 16:50:19 +0100 Subject: [PATCH 3/5] refactor(internal): WB-05 retire internal mesh.data references (mesh.X.coords) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave B of the 2026-07 remediation worklist. The audit's ~14 mesh.data sites had already been reduced to zero live coordinate reads by earlier waves; what remained in src/ were docstring examples teaching the deprecated pattern, plus one live 'with self.access(var)' wrapper in Mesh.update_lvec's field-decomposition loop (the wrapper only deferred write callbacks around a pure .vec read, which lazily creates the vector itself — removal is a no-op). - coordinates.py: two geographic-conversion docstring examples -> mesh.X.coords - functions_unit_system.py: evaluate() docstring example -> mesh.X.coords - surfaces.py: transfer_normals docstring text (code already reads the mesh's own vertex coordinates) - nd_array_callback.py: delay_callbacks_global example rewritten with modern .array writes - discretisation_mesh.py: drop the redundant access() wrapper in update_lvec The mesh.data deprecation property itself (and Mesh._legacy_access) are intentionally untouched; the mesh.adapt() docstring example is left for WE-07, which owns it. Underworld development team with AI support from Claude Code --- src/underworld3/coordinates.py | 5 +++-- src/underworld3/discretisation/discretisation_mesh.py | 5 ++--- src/underworld3/function/functions_unit_system.py | 2 +- src/underworld3/meshing/surfaces.py | 3 ++- src/underworld3/utilities/nd_array_callback.py | 6 +++--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/underworld3/coordinates.py b/src/underworld3/coordinates.py index 3f50b90b1..a8a0b71a7 100644 --- a/src/underworld3/coordinates.py +++ b/src/underworld3/coordinates.py @@ -770,7 +770,8 @@ def from_cartesian(self, x, y, z): Examples -------- >>> # Convert mesh points to geographic for comparison with data - >>> x, y, z = mesh.data[:, 0], mesh.data[:, 1], mesh.data[:, 2] + >>> coords = mesh.X.coords + >>> x, y, z = coords[:, 0], coords[:, 1], coords[:, 2] >>> lon, lat, depth = mesh.geo.from_cartesian(x, y, z) """ # Nondimensionalise ellipsoid for numeric coordinate conversion @@ -834,7 +835,7 @@ def points_from_cartesian(self, points_xyz): Examples -------- >>> # Export mesh coordinates to geographic - >>> mesh_xyz = mesh.data # or mesh.CoordinateSystem.coords + >>> mesh_xyz = mesh.X.coords >>> mesh_llz = mesh.geo.points_from_cartesian(mesh_xyz) """ import numpy as np diff --git a/src/underworld3/discretisation/discretisation_mesh.py b/src/underworld3/discretisation/discretisation_mesh.py index 85a03d03a..8859464a4 100644 --- a/src/underworld3/discretisation/discretisation_mesh.py +++ b/src/underworld3/discretisation/discretisation_mesh.py @@ -2984,9 +2984,8 @@ def update_lvec(self, swarm_sync=True): # traverse subdms, taking user generated data in the subdm # local vec, pushing it into a global sub vec for var, subiset, subdm in zip(self.vars.values(), isets, dms): - # Use access pattern to ensure vector is available - with self.access(var): - lvec = var.vec + # var.vec lazily creates the PETSc local vector on first access + lvec = var.vec subvec = a_global.getSubVector(subiset) subdm.localToGlobal(lvec, subvec, addv=False) a_global.restoreSubVector(subiset, subvec) diff --git a/src/underworld3/function/functions_unit_system.py b/src/underworld3/function/functions_unit_system.py index b567081da..4d7c0a31a 100644 --- a/src/underworld3/function/functions_unit_system.py +++ b/src/underworld3/function/functions_unit_system.py @@ -147,7 +147,7 @@ def _evaluate_impl( -------- >>> # Works with both dimensional and non-dimensional coords >>> result = uw.function.evaluate(T.sym, T.coords) # dimensional coords - >>> result = uw.function.evaluate(T.sym, mesh.data[:, :2]) # non-dimensional + >>> result = uw.function.evaluate(T.sym, mesh.X.coords[:, :2]) # non-dimensional >>> if hasattr(result, 'to'): ... result_K = result.to('K') # Unit conversion """ diff --git a/src/underworld3/meshing/surfaces.py b/src/underworld3/meshing/surfaces.py index 21191979b..ed3511f80 100644 --- a/src/underworld3/meshing/surfaces.py +++ b/src/underworld3/meshing/surfaces.py @@ -2122,7 +2122,8 @@ def transfer_normals( Args: mesh: The mesh to transfer normals to - coords: Optional coordinates to query. If None, uses mesh.data + coords: Optional coordinates to query. If None, uses the mesh's + own vertex coordinates (model space) normal_var: Optional existing MeshVariable variable_name: Name for new variable diff --git a/src/underworld3/utilities/nd_array_callback.py b/src/underworld3/utilities/nd_array_callback.py index 5ce1b0867..d0bafe87e 100644 --- a/src/underworld3/utilities/nd_array_callback.py +++ b/src/underworld3/utilities/nd_array_callback.py @@ -421,9 +421,9 @@ def delay_callbacks_global(context_info=None): Example ------- - >>> with NDArray_With_Callback.delay_callbacks_global("mesh update"): - ... mesh.data[0] = new_pos - ... swarm.data += displacement + >>> with NDArray_With_Callback.delay_callbacks_global("field update"): + ... temperature.array[...] = new_T + ... material.array[...] = new_material # All callbacks from all arrays fire here """ From d02a25f8724bc99b50ac0a25588f264b22d35590 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 6 Jul 2026 16:50:40 +0100 Subject: [PATCH 4/5] refactor(internal): WB-06 delete Swarm._legacy_access (zero callers) With WB-01 landed there are no remaining internal callers of the old context-managed access machinery on the swarm side. Delete Swarm._legacy_access (the pre-2025 getField/restoreField context manager with its exit_manager migration/proxy-update hooks) and the now-orphaned SwarmVariable._is_accessed init that only it consulted. Verified by grep across src/ and tests/: the only _legacy_access references left are Mesh._legacy_access (out of scope for WB-06) and comments. The deprecated public Swarm.access() compatibility shim is retained unchanged as the documented entry point for legacy user code. Underworld development team with AI support from Claude Code --- src/underworld3/swarm.py | 142 --------------------------------------- 1 file changed, 142 deletions(-) diff --git a/src/underworld3/swarm.py b/src/underworld3/swarm.py index cba131666..f60921bb6 100644 --- a/src/underworld3/swarm.py +++ b/src/underworld3/swarm.py @@ -317,7 +317,6 @@ def __init__( # add to swarms dict self.swarm._vars[self.clean_name] = self - self._is_accessed = False # Initialize proxy flags first before creating proxy variable self._updating_proxy = False # Flag to prevent recursive proxy updates @@ -4593,147 +4592,6 @@ def apply_snapshot_payload(self, payload: dict) -> None: # that garbage (exposed by the SWARM-01 invalidation fix). var.data[...] = saved - def _legacy_access(self, *writeable_vars: SwarmVariable): - """ - This context manager makes the underlying swarm variables data available to - the user. The data should be accessed via the variables `data` handle. - - As default, all data is read-only. To enable writeable data, the user should - specify which variable they wish to modify. - - At the conclusion of the users context managed block, numerous further operations - will be automatically executed. This includes swarm parallel migration routines - where the swarm's `particle_coordinates` variable has been modified. The swarm - variable proxy mesh variables will also be updated for modifed swarm variables. - - Parameters - ---------- - writeable_vars - The variables for which data write access is required. - - Example - ------- - - >>> import underworld3 as uw - >>> someMesh = uw.discretisation.FeMesh_Cartesian() - >>> with someMesh._deform_mesh(): - ... someMesh.data[0] = [0.1,0.1] - >>> someMesh.data[0] - array([ 0.1, 0.1]) - """ - import time - - uw.timing._incrementDepth() - stime = time.time() - - deaccess_list = [] - for var in self._vars.values(): - # if already accessed within higher level context manager, continue. - if var._is_accessed == True: - continue - # set flag so variable status can be known elsewhere - var._is_accessed = True - # add to de-access list to rewind this later - deaccess_list.append(var) - # grab numpy object, setting read only if necessary - var._data = self.dm.getField(var.clean_name).reshape((-1, var.num_components)) - assert var._data is not None - if var not in writeable_vars: - var._old_data_flag = var._data.flags.writeable - var._data.flags.writeable = False - else: - # increment variable state - var._increment() - - # make *view* for each var component - if var._proxy: - for i in range(0, var.shape[0]): - for j in range(0, var.shape[1]): - var._data_container[i, j] = var._data_container[i, j]._replace( - data=var._data[:, var._data_layout(i, j)], - ) - - # if particles moving, update swarm state - if self._particle_coordinates in writeable_vars: - self._increment() - - # Create a class which specifies the required context - # manager hooks (`__enter__`, `__exit__`). - class exit_manager: - def __init__(self, swarm): - self.em_swarm = swarm - - def __enter__(self): - - pass - - def __exit__(self, *args): - - for var in self.em_swarm.vars.values(): - # only de-access variables we have set access for. - if var not in deaccess_list: - continue - # set this back, although possibly not required. - if var not in writeable_vars: - var._data.flags.writeable = var._old_data_flag - var._data = None - self.em_swarm.dm.restoreField(var.clean_name) - var._is_accessed = False - # do particle migration if coords changes - - if self.em_swarm._particle_coordinates in writeable_vars: - # let's use the mesh index to update the particles owning cells. - # note that the `petsc4py` interface is more convenient here as the - # `SwarmVariable.data` interface is controlled by the context manager - # that we are currently within, and it is therefore too easy to - # get things wrong that way. - # - # - - # if uw.mpi.size > 1: - # coords = self.em_swarm.dm.getField("DMSwarmPIC_coor").reshape( - # (-1, self.em_swarm.dim) - # ) - - # self.em_swarm.dm.restoreField("DMSwarmPIC_coor") - - # ## We'll need to identify the new processes here and update the particle rank value accordingly - # - - # Even if only on one process, migrate needs to be called to remove particles that are - # not in the domain. - - self.em_swarm.migrate( - remove_sent_points=True, - delete_lost_points=self.em_swarm._clip_to_mesh, - ) - - # void these things too - self.em_swarm._index = None - self.em_swarm._nnmapdict = {} - - # do var updates - for var in self.em_swarm.vars.values(): - # if swarm migrated, update all. - # if var updated, update var. - if (self.em_swarm._particle_coordinates in writeable_vars) or ( - var in writeable_vars - ): - var._update() - - if var._proxy: - for i in range(0, var.shape[0]): - for j in range(0, var.shape[1]): - # var._data_ij[i, j] = None - var._data_container[i, j] = var._data_container[i, j]._replace( - data=f"SwarmVariable[...].data is only available within mesh.access() context", - ) - - uw.timing._decrementDepth() - uw.timing.log_result(time.time() - stime, "Swarm.access", 1) - - return exit_manager(self) - def access(self, *writeable_vars: SwarmVariable): """ Dummy access manager that provides deferred sync for backward compatibility. From 0f96d4b0acb9a643c9cb887ccfc7ccde9c10f39e Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 6 Jul 2026 16:52:06 +0100 Subject: [PATCH 5/5] refactor(internal): WB-04 drop the one live access() wrapper in _function.pyx Wave B pyx verification (2026-07 remediation worklist). The seven deprecated-pattern references the audit indexed in cython/petsc_generic_snes_solvers.pyx are all commented-out fossils (currently lines 3146, 3187, 4212, 4259, 8225, 8315, 8359); per the pyx rule they are left for the Wave A/D pyx sub-wave (WA-07) rather than touched here. Grep did find one LIVE site outside that file: _function.pyx wrapped a single work_var.data write in 'with mesh.access(work_var):'. mesh.access is the no-op deferred-callback shim, so removing the wrapper around one direct .data write is the sanctioned trivially-local migration allowed by the WB-04 rule. Underworld development team with AI support from Claude Code --- src/underworld3/function/_function.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/underworld3/function/_function.pyx b/src/underworld3/function/_function.pyx index d07eb41eb..c1d1bce3e 100644 --- a/src/underworld3/function/_function.pyx +++ b/src/underworld3/function/_function.pyx @@ -892,8 +892,7 @@ def _clement_to_work_variable(expr, mesh, derivfns): result = np.full(n_nodes, result[0]) # Store in work variable - with mesh.access(work_var): - work_var.data[:, 0] = result.flatten() + work_var.data[:, 0] = result.flatten() return work_var