Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/underworld3/cython/petsc_maths.pyx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Union
import sympy
import numpy as np

import underworld3
import underworld3.timing as timing
Expand All @@ -15,6 +16,31 @@ cdef extern from "petsc.h" nogil:
PetscErrorCode DMPlexComputeCellwiseIntegralFEM( PetscDM, PetscVec, PetscVec, void* )


def _pack_manifest(manifest):
"""The current values of the JIT constants manifest as a contiguous array."""
from underworld3.utilities._jitextension import _pack_constants
if not manifest:
return None
return np.ascontiguousarray(_pack_constants(manifest), dtype=np.float64)


cdef _set_ds_constants(PetscDS ds, manifest):
"""Hand the current UWexpression values to the DS the integral kernel reads.

The JIT routes every ``uw.function.expression`` in the integrand to PETSc's
constants array (the same mechanism the solvers use, so a changed value does
not recompile). A DS that never receives the values hands the kernel zeros:
a viscosity, a time or any other expression in an integrand silently
integrated to nothing (found on the cylinder drag, 2026-09-05).
"""
cdef double[::1] vals
values = _pack_manifest(manifest)
if values is None or len(values) == 0:
return
vals = values
CHKERRQ(PetscDSSetConstants(ds, len(values), <const PetscScalar*>&vals[0]))


def dm_force_coordinate_field(dm):
"""Force coordinate field creation and strip boundary labels from the
coordinate DM. Must be called after createCoordinateSpace and after
Expand Down Expand Up @@ -122,8 +148,9 @@ class Integral:
cdef DS ds = self.dm.getDS()
cdef PetscScalar val_array[256]

# Now set callback...
# Now set callback (and the current constant values the kernel reads)...
ierr = PetscDSSetObjective(ds.ds, 0, ext.fns_residual[0]); CHKERRQ(ierr)
_set_ds_constants(ds.ds, _getext_result.constants_manifest)
ierr = DMPlexComputeIntegralFEM(dm.dm, cgvec.vec, &(val_array[0]), NULL); CHKERRQ(ierr)

self.dm.restoreGlobalVec(a_global)
Expand Down Expand Up @@ -290,8 +317,9 @@ class CellWiseIntegral:
elif isinstance(self.fn, sympy.vector.Dyadic):
raise RuntimeError("Integral evaluation for Dyadic integrands not supported.")

cdef PtrContainer ext = getext(self.mesh, JITCallbackSet(residual=(self.fn,)),
self.mesh.vars.values()).ptrobj
_getext_result = getext(self.mesh, JITCallbackSet(residual=(self.fn,)),
self.mesh.vars.values())
cdef PtrContainer ext = _getext_result.ptrobj

# Pull out vec for variables, and go ahead with the integral
self.mesh.update_lvec()
Expand All @@ -316,6 +344,7 @@ class CellWiseIntegral:
cdef DM dm = self.mesh.dm
cdef DS ds = self.mesh.dm.getDS()
CHKERRQ( PetscDSSetObjective(ds.ds, 0, ext.fns_residual[0]) )
_set_ds_constants(ds.ds, _getext_result.constants_manifest)

# DMPlexComputeCellwiseIntegralFEM writes Nf scalars per cell into a
# flat [cell*Nf + field] layout when the output vector carries no
Expand Down Expand Up @@ -461,6 +490,11 @@ class BdIntegral:
cdef PetscDMLabel sandbox_label = NULL
CHKERRQ(DMGetLabel(sandbox_dm, boundary_bytes, &sandbox_label))

# The sandbox has its own DS (DMCreateDS): the constants go there.
cdef PetscDS sandbox_ds = NULL
CHKERRQ(DMGetDS(sandbox_dm, &sandbox_ds))
_set_ds_constants(sandbox_ds, _getext_result.constants_manifest)

# Output value
cdef PetscScalar result = 0.0

Expand Down
64 changes: 64 additions & 0 deletions tests/test_0503_integral_expression_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""A `uw.function.expression` inside an integrand must reach the integral kernel.

The JIT routes every expression constant to PETSc's constants array (so that a
changed value does not recompile). The integral classes compiled through that
path but never set the values on the DS they integrate with, so the kernel read
zeros: any integrand carrying a viscosity, a time, or any other expression
integrated to nothing, and a fresh Integral returned the same zero from the
cache. Found on the cylinder drag (the viscous traction vanished), 2026-09-05.
"""
import numpy as np
import pytest
import sympy
import underworld3 as uw

pytestmark = [pytest.mark.level_1, pytest.mark.tier_a]


@pytest.fixture(scope="module")
def setup():
mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0),
cellSize=0.25, regular=True, qdegree=3)
x, y = mesh.X
T = uw.discretisation.MeshVariable("T_c", mesh, 1, degree=2)
T.array[:, 0, 0] = uw.function.evaluate(y ** 2, T.coords).reshape(-1) # dT/dy = 2y
return mesh, x, y, T


def test_volume_integral_carries_the_expression_value(setup):
mesh, x, y, T = setup
c = uw.function.expression(r"c_{v}", 2.0, "probe constant")
integral = uw.maths.Integral(mesh, c * T.sym[0].diff(y)) # 2 * int 2y = 2
assert np.isclose(integral.evaluate(), 2.0, rtol=1e-8)
c.sym = 3.0 # a changed value, no recompile
assert np.isclose(integral.evaluate(), 3.0, rtol=1e-8)
assert np.isclose(uw.maths.Integral(mesh, c * T.sym[0].diff(y)).evaluate(), 3.0, rtol=1e-8)


def test_boundary_integral_carries_the_expression_value(setup):
mesh, x, y, T = setup
c = uw.function.expression(r"c_{b}", 2.0, "probe constant")
integral = uw.maths.BdIntegral(mesh, c * T.sym[0].diff(y), "Top") # 2 * 2 * length 1
assert np.isclose(integral.evaluate(), 4.0, rtol=1e-8)
c.sym = 0.5
assert np.isclose(integral.evaluate(), 1.0, rtol=1e-8)


def test_cellwise_integral_carries_the_expression_value(setup):
mesh, x, y, T = setup
c = uw.function.expression(r"c_{c}", 2.0, "probe constant")
cells = uw.maths.CellWiseIntegral(mesh, c * T.sym[0].diff(y)).evaluate()
assert np.isclose(np.asarray(cells).sum(), 2.0, rtol=1e-8)


def test_constitutive_flux_in_a_boundary_integral(setup):
"""The case that found it: the viscous traction on a wall."""
mesh, x, y, T = setup
v = uw.discretisation.MeshVariable("U_c", mesh, 2, degree=2)
p = uw.discretisation.MeshVariable("P_c", mesh, 1, degree=1)
stokes = uw.systems.Stokes(mesh, v, p)
stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
stokes.constitutive_model.Parameters.shear_viscosity_0 = 2.0
v.array[:, 0, :] = uw.function.evaluate(sympy.Matrix([[y ** 2, 0.0]]), v.coords).reshape(-1, 2)
sigma_xy = stokes.constitutive_model.flux[0, 1] # 2 eta (du/dy)/2 = 2y * 2 / ... = eta * 2y
assert np.isclose(uw.maths.BdIntegral(mesh, sigma_xy, "Top").evaluate(), 4.0, rtol=1e-8)
Loading