Skip to content

Commit 4e9f8df

Browse files
committed
Merge development (post-#186 merge) — combine theta and monotone_mode params
2 parents 290951b + ae4b447 commit 4e9f8df

10 files changed

Lines changed: 517 additions & 111 deletions

src/underworld3/cython/petsc_types.pyx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1-
from libc.stdlib cimport malloc
1+
from libc.stdlib cimport malloc, free
22

33
cdef class PtrContainer:
44

5+
def __cinit__(self):
6+
self.fns_residual = NULL
7+
self.fns_bcs = NULL
8+
self.fns_jacobian = NULL
9+
self.fns_bd_residual = NULL
10+
self.fns_bd_jacobian = NULL
11+
12+
def __dealloc__(self):
13+
if self.fns_residual != NULL:
14+
free(self.fns_residual)
15+
if self.fns_bcs != NULL:
16+
free(self.fns_bcs)
17+
if self.fns_jacobian != NULL:
18+
free(self.fns_jacobian)
19+
if self.fns_bd_residual != NULL:
20+
free(self.fns_bd_residual)
21+
if self.fns_bd_jacobian != NULL:
22+
free(self.fns_bd_jacobian)
23+
524
cpdef allocate(self, int n_res, int n_bcs, int n_jac, int n_bd_res, int n_bd_jac):
625
"""Allocate function pointer arrays of the given sizes."""
26+
27+
# Free existing memory if already allocated
28+
if self.fns_residual != NULL: free(self.fns_residual)
29+
if self.fns_bcs != NULL: free(self.fns_bcs)
30+
if self.fns_jacobian != NULL: free(self.fns_jacobian)
31+
if self.fns_bd_residual != NULL: free(self.fns_bd_residual)
32+
if self.fns_bd_jacobian != NULL: free(self.fns_bd_jacobian)
33+
734
self.fns_residual = <PetscDSResidualFn*> malloc(n_res * sizeof(PetscDSResidualFn))
835
self.fns_bcs = <PetscDSResidualFn*> malloc(n_bcs * sizeof(PetscDSResidualFn))
936
self.fns_jacobian = <PetscDSJacobianFn*> malloc(n_jac * sizeof(PetscDSJacobianFn))

src/underworld3/discretisation/discretisation_mesh.py

Lines changed: 104 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ def __init__(
285285
self._registered_swarms = weakref.WeakSet()
286286
self._registered_surfaces = weakref.WeakSet() # Surfaces using this mesh
287287
self._registered_submeshes = weakref.WeakSet() # Submeshes from extract_region
288+
289+
# _mesh_update_lock: Re-entrant lock to coordinate mesh deformation.
290+
# Held by mesh_update_callback during _deform_mesh(). Checked by
291+
# MeshVariable callbacks (blocking=False) to skip PETSc sync during
292+
# sensitive coordinate changes.
288293
self._mesh_update_lock = threading.RLock()
289294

290295
comm = PETSc.COMM_WORLD
@@ -594,14 +599,25 @@ class replacement_boundaries(Enum):
594599
# to handle that so we just wrap it here.
595600

596601
def mesh_update_callback(array, change_context):
597-
print(f"Mesh update callback - mesh deform")
598-
coords = array.reshape(-1, array.owner.cdim)
599-
self._deform_mesh(coords, verbose=True)
602+
mesh = array.owner
603+
if mesh is None:
604+
# This guard handles cases where the array is accessed during
605+
# object teardown (e.g. at application exit or during mesh
606+
# replacement), where the owning Python mesh object has already
607+
# been garbage collected but the NDArray proxy still exists.
608+
return
609+
610+
if verbose:
611+
uw.pprint(0, f"Mesh update callback - mesh deform")
612+
613+
coords = array.reshape(-1, mesh.cdim)
614+
mesh._deform_mesh(coords, verbose=verbose)
600615

601616
# Increment mesh version to notify registered swarms of coordinate changes
602-
with self._mesh_update_lock:
603-
self._mesh_version += 1
604-
print(f"Mesh version incremented to {self._mesh_version}")
617+
with mesh._mesh_update_lock:
618+
mesh._mesh_version += 1
619+
if verbose:
620+
uw.pprint(0, f"Mesh version incremented to {mesh._mesh_version}")
605621

606622
return
607623

@@ -1239,11 +1255,12 @@ def _build_vertex_map(self):
12391255
Uses coordinate matching at extraction time (before any
12401256
deformation). Cached permanently since topology doesn't change.
12411257
"""
1242-
if hasattr(self, '_vertex_map') and self._vertex_map is not None:
1258+
if hasattr(self, "_vertex_map") and self._vertex_map is not None:
12431259
return self._vertex_map
12441260

1245-
tree = uw.kdtree.KDTree(self.X.coords)
1246-
dists, indices = tree.query(self.parent.X.coords, sqr_dists=False)
1261+
# Use cached KDTree from coordinate variable
1262+
tree = self.X._get_kdtree()
1263+
dists, indices = tree.query(self.parent.X.coords_nd, sqr_dists=False)
12471264
matched = dists < 1.0e-10
12481265

12491266
# parent_rows[i] -> sub_rows[i]: matched vertex pairs
@@ -1337,10 +1354,14 @@ def _re_extract_from_parent(self, verbose=False):
13371354
)
13381355

13391356
def mesh_update_callback(array, change_context):
1340-
coords = array.reshape(-1, array.owner.cdim)
1341-
self._deform_mesh(coords, verbose=False)
1342-
with self._mesh_update_lock:
1343-
self._mesh_version += 1
1357+
mesh = array.owner
1358+
if mesh is None:
1359+
return
1360+
1361+
coords = array.reshape(-1, mesh.cdim)
1362+
mesh._deform_mesh(coords, verbose=False)
1363+
with mesh._mesh_update_lock:
1364+
mesh._mesh_version += 1
13441365
return
13451366

13461367
self._coords.add_callback(mesh_update_callback)
@@ -1433,8 +1454,8 @@ def _build_dof_map(self, parent_var, sub_var):
14331454
if key in self._dof_maps:
14341455
return self._dof_maps[key]
14351456

1436-
tree = uw.kdtree.KDTree(sub_var.coords)
1437-
dists, indices = tree.query(parent_var.coords, sqr_dists=False)
1457+
tree = sub_var._get_kdtree()
1458+
dists, indices = tree.query(parent_var.coords_nd, sqr_dists=False)
14381459
matched = dists < 1.0e-10
14391460

14401461
# indices[matched] maps parent row → sub row
@@ -1777,42 +1798,56 @@ def _deform_mesh(self, new_coords: numpy.ndarray, verbose=False):
17771798
The coord array that is passed in should match the shape of self.data
17781799
"""
17791800

1780-
coord_vec = self.dm.getCoordinatesLocal()
1781-
coords = coord_vec.array.reshape(-1, self.cdim)
1782-
coords[...] = new_coords[...]
1783-
1784-
self.dm.setCoordinatesLocal(coord_vec)
1785-
self.nuke_coords_and_rebuild()
1786-
1787-
# Rebuild the _coords array view. nuke_coords_and_rebuild may
1788-
# replace the coordinate vector internally (createCoordinateSpace),
1789-
# leaving self._coords as a stale numpy view of the old buffer.
1790-
import underworld3.utilities
1791-
old_callbacks = getattr(self._coords, "_callbacks", [])
1792-
self._coords = underworld3.utilities.NDArray_With_Callback(
1793-
numpy.ndarray.view(
1794-
self.dm.getCoordinatesLocal().array.reshape(-1, self.cdim)
1795-
),
1796-
owner=self,
1797-
)
1798-
for cb in old_callbacks:
1799-
self._coords.add_callback(cb)
1800-
1801-
# BUGFIX(#122): mark registered solvers for rebuild. Since PR #127
1802-
# ("Trust JIT cache: skip DM rebuild on constant-only parameter
1803-
# changes") a solver with is_setup=True trusts its cached PETSc DM
1804-
# / SNES assembly and skips rebuild on the next solve(). After a
1805-
# coordinate change the cached DM still carries pre-deform
1806-
# coordinates, so F(v_prev) ≈ 0 and the solver converges in 0
1807-
# iterations without updating the solution. mesh.adapt() already
1808-
# does this; _deform_mesh must match.
1809-
for solver in self._equation_systems_register:
1810-
if solver is not None and hasattr(solver, "is_setup"):
1811-
solver.is_setup = False
1801+
with self._mesh_update_lock:
1802+
coord_vec = self.dm.getCoordinatesLocal()
1803+
coords = coord_vec.array.reshape(-1, self.cdim)
1804+
coords[...] = new_coords[...]
1805+
1806+
self.dm.setCoordinatesLocal(coord_vec)
1807+
self.nuke_coords_and_rebuild()
1808+
1809+
# Rebuild the _coords array view. nuke_coords_and_rebuild may
1810+
# replace the coordinate vector internally (createCoordinateSpace),
1811+
# leaving self._coords as a stale numpy view of the old buffer.
1812+
import underworld3.utilities
1813+
old_callbacks = getattr(self._coords, "_callbacks", [])
1814+
self._coords = underworld3.utilities.NDArray_With_Callback(
1815+
numpy.ndarray.view(
1816+
self.dm.getCoordinatesLocal().array.reshape(-1, self.cdim)
1817+
),
1818+
owner=self,
1819+
)
1820+
for cb in old_callbacks:
1821+
self._coords.add_callback(cb)
1822+
1823+
# BUGFIX(#122): mark registered solvers for rebuild. Since PR #127
1824+
# ("Trust JIT cache: skip DM rebuild on constant-only parameter
1825+
# changes") a solver with is_setup=True trusts its cached PETSc DM
1826+
# / SNES assembly and skips rebuild on the next solve(). After a
1827+
# coordinate change the cached DM still carries pre-deform
1828+
# coordinates, so F(v_prev) ≈ 0 and the solver converges in 0
1829+
# iterations without updating the solution. mesh.adapt() already
1830+
# does this; _deform_mesh must match.
1831+
for solver in self._equation_systems_register:
1832+
if solver is not None and hasattr(solver, "is_setup"):
1833+
solver.is_setup = False
1834+
1835+
# Invalidate caches whose contents become stale when mesh
1836+
# coordinates change. Matches the cache hygiene already
1837+
# performed by mesh.adapt() and _legacy_access. Without
1838+
# these, uw.function.evaluate (and any user code that keys
1839+
# lookups off _topology_version) can return values
1840+
# computed against the pre-deform mesh.
1841+
self._evaluation_hash = None
1842+
self._evaluation_interpolated_results = None
1843+
if hasattr(self, '_dminterpolation_cache'):
1844+
self._dminterpolation_cache.invalidate_all(
1845+
reason="mesh deformed")
1846+
self._topology_version += 1
18121847

1813-
# Propagate coordinate changes to registered submeshes
1814-
for submesh in self._registered_submeshes:
1815-
submesh.sync_coordinates_from_parent()
1848+
# Propagate coordinate changes to registered submeshes
1849+
for submesh in self._registered_submeshes:
1850+
submesh.sync_coordinates_from_parent()
18161851

18171852
return
18181853

@@ -3455,11 +3490,24 @@ def _get_mesh_centroids(self):
34553490
def _get_domain_centroids(self):
34563491

34573492
import numpy as np
3493+
from underworld3.utilities import gather_data
34583494

34593495
domain_centroid = self._centroids.mean(axis=0)
34603496
all_centroids = gather_data(domain_centroid, bcast=True).reshape(-1, self.dim)
34613497
return all_centroids
34623498

3499+
def _get_domain_kdtree(self):
3500+
import underworld3 as uw
3501+
if (
3502+
not hasattr(self, "_domain_kdtree")
3503+
or self._domain_kdtree is None
3504+
or getattr(self, "_domain_kdtree_version", -1) != self._mesh_version
3505+
):
3506+
centroids = self._get_domain_centroids()
3507+
self._domain_kdtree = uw.kdtree.KDTree(centroids)
3508+
self._domain_kdtree_version = self._mesh_version
3509+
return self._domain_kdtree
3510+
34633511
def get_min_radius_old(self) -> float:
34643512
"""
34653513
This method returns the global minimum distance from any cell centroid to a face.
@@ -3777,12 +3825,15 @@ def adapt(self, metric_field, verbose=False):
37773825

37783826
# Rebuild the callback for mesh deformation
37793827
def mesh_update_callback(array, change_context):
3780-
print(f"Mesh update callback - mesh deform")
3828+
if verbose:
3829+
uw.pprint(0, f"Mesh update callback - mesh deform")
3830+
37813831
coords = array.reshape(-1, array.owner.cdim)
3782-
self._deform_mesh(coords, verbose=True)
3832+
self._deform_mesh(coords, verbose=verbose)
37833833
with self._mesh_update_lock:
37843834
self._mesh_version += 1
3785-
print(f"Mesh version incremented to {self._mesh_version}")
3835+
if verbose:
3836+
uw.pprint(0, f"Mesh version incremented to {self._mesh_version}")
37863837
return
37873838

37883839
self._coords.add_callback(mesh_update_callback)

0 commit comments

Comments
 (0)