drbx 2.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drbx/__init__.py +20 -0
- drbx/__main__.py +5 -0
- drbx/cli.py +586 -0
- drbx/config/__init__.py +20 -0
- drbx/config/boutinp.py +556 -0
- drbx/config/model.py +34 -0
- drbx/config/normalization.py +66 -0
- drbx/data/atomic_rates/__init__.py +1 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
- drbx/geometry/__init__.py +207 -0
- drbx/geometry/embedding.py +56 -0
- drbx/geometry/essos_import.py +1385 -0
- drbx/geometry/fci_geometry.py +4622 -0
- drbx/geometry/fci_maps.py +85 -0
- drbx/geometry/island_divertor.py +291 -0
- drbx/geometry/metric_tensor.py +99 -0
- drbx/geometry/open_slab.py +150 -0
- drbx/geometry/rotating_ellipse.py +253 -0
- drbx/geometry/shifted_torus.py +225 -0
- drbx/geometry/stellarator.py +287 -0
- drbx/geometry/vmec_extender_import.py +499 -0
- drbx/geometry/vmec_jax_import.py +306 -0
- drbx/linear/__init__.py +37 -0
- drbx/linear/dispersion.py +138 -0
- drbx/linear/eigen.py +91 -0
- drbx/native/__init__.py +224 -0
- drbx/native/array_backend.py +64 -0
- drbx/native/deck_runner.py +779 -0
- drbx/native/electromagnetic.py +250 -0
- drbx/native/expression.py +173 -0
- drbx/native/fci.py +295 -0
- drbx/native/fci_2_field_rhs.py +182 -0
- drbx/native/fci_4_field_rhs.py +1267 -0
- drbx/native/fci_boundaries.py +2494 -0
- drbx/native/fci_differentiable_case.py +304 -0
- drbx/native/fci_drb_EB_rhs.py +1243 -0
- drbx/native/fci_drb_rhs.py +190 -0
- drbx/native/fci_halo.py +1575 -0
- drbx/native/fci_helpers.py +350 -0
- drbx/native/fci_model.py +294 -0
- drbx/native/fci_neutral.py +139 -0
- drbx/native/fci_operators.py +4081 -0
- drbx/native/fci_sharding.py +597 -0
- drbx/native/fci_sheath_recycling.py +206 -0
- drbx/native/fci_time_integrator.py +96 -0
- drbx/native/fci_vorticity.py +198 -0
- drbx/native/fluid_1d.py +330 -0
- drbx/native/hasegawa_wakatani.py +196 -0
- drbx/native/limiters.py +57 -0
- drbx/native/mesh.py +238 -0
- drbx/native/metrics.py +234 -0
- drbx/native/neutrals/__init__.py +58 -0
- drbx/native/neutrals/atomic_rates.py +134 -0
- drbx/native/neutrals/detachment_sol_model.py +221 -0
- drbx/native/neutrals/reactions.py +164 -0
- drbx/native/neutrals/recycling_sol_model.py +197 -0
- drbx/native/sol_flux_tube.py +133 -0
- drbx/native/stellarator_turbulence.py +343 -0
- drbx/native/transport.py +134 -0
- drbx/native/units.py +32 -0
- drbx/native/vorticity.py +252 -0
- drbx/runtime/__init__.py +53 -0
- drbx/runtime/artifacts.py +161 -0
- drbx/runtime/memory.py +144 -0
- drbx/runtime/output.py +374 -0
- drbx/runtime/paths.py +9 -0
- drbx/runtime/performance.py +161 -0
- drbx/runtime/run_config.py +184 -0
- drbx/runtime/scheduler.py +99 -0
- drbx/runtime/state.py +40 -0
- drbx/validation/__init__.py +424 -0
- drbx/validation/autodiff_diffusion.py +329 -0
- drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
- drbx/validation/diverted_tokamak_movie.py +558 -0
- drbx/validation/essos_fieldline_import_campaign.py +181 -0
- drbx/validation/essos_imported_artifact_audit.py +535 -0
- drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
- drbx/validation/essos_imported_fci_campaign.py +5241 -0
- drbx/validation/essos_imported_pytree_campaign.py +406 -0
- drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
- drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
- drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
- drbx/validation/fluid_1d_mms_convergence.py +250 -0
- drbx/validation/geometry_lineouts.py +136 -0
- drbx/validation/geometry_slices.py +178 -0
- drbx/validation/publication_plotting.py +91 -0
- drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
- drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
- drbx/validation/stellarator_fci_operator_campaign.py +304 -0
- drbx/validation/stellarator_fci_suite_campaign.py +264 -0
- drbx/validation/stellarator_metric_mms_campaign.py +289 -0
- drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
- drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
- drbx/validation/stellarator_sol_showcase.py +628 -0
- drbx/validation/stellarator_vorticity_campaign.py +304 -0
- drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
- drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
- drbx-2.0.0.dev0.dist-info/METADATA +380 -0
- drbx-2.0.0.dev0.dist-info/RECORD +106 -0
- drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
- drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
- drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- drbx-2.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2494 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
|
|
9
|
+
from ..geometry.fci_geometry import (
|
|
10
|
+
_DataclassPyTreeMixin,
|
|
11
|
+
FciGeometry3D,
|
|
12
|
+
LocalDomain3D,
|
|
13
|
+
LocalFciGeometry3D,
|
|
14
|
+
HaloLayout3D,
|
|
15
|
+
LocalRegularFaceGeometry3D,
|
|
16
|
+
LocalCellVolumeGeometry3D,
|
|
17
|
+
RegularFaceGeometry3D,
|
|
18
|
+
CellVolumeGeometry3D,
|
|
19
|
+
)
|
|
20
|
+
from .fci_model import (
|
|
21
|
+
FciFieldBundle,
|
|
22
|
+
FciModelState,
|
|
23
|
+
assert_matching_field_names,
|
|
24
|
+
)
|
|
25
|
+
from .fci_helpers import (
|
|
26
|
+
_as_bool_face_array,
|
|
27
|
+
_as_coordinate_derivative_weight_array,
|
|
28
|
+
_as_coordinate_face_tuple,
|
|
29
|
+
_as_face_flux_array,
|
|
30
|
+
_as_float64_array,
|
|
31
|
+
_as_int_face_array,
|
|
32
|
+
_as_int_stencil_array,
|
|
33
|
+
_as_wall_face_array,
|
|
34
|
+
_as_weight_stencil_array,
|
|
35
|
+
_axis_regular_lower_x_face,
|
|
36
|
+
_normalize_axis_flags,
|
|
37
|
+
_local_cell_halo_array,
|
|
38
|
+
_as_local_wall_array,
|
|
39
|
+
_as_local_wall_int_array,
|
|
40
|
+
_as_local_wall_bool_array,
|
|
41
|
+
_as_local_wall_stencil_index_array,
|
|
42
|
+
_as_local_wall_stencil_weight_array,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_pytree_base = jax.tree_util.register_pytree_node_class
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
BC_NONE = 0
|
|
50
|
+
BC_DIRICHLET = 1
|
|
51
|
+
BC_NEUMANN = 2
|
|
52
|
+
BC_NORMALFLUX = 3
|
|
53
|
+
BC_NOFLUX = 4
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
BoundaryPayloadT = TypeVar("BoundaryPayloadT")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@_pytree_base
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class LocalBoundaryData3D(_DataclassPyTreeMixin):
|
|
62
|
+
"""Model-shaped local boundary payload bundle.
|
|
63
|
+
|
|
64
|
+
``face_bc`` and ``cut_wall_bc`` are field bundles: each bundle field names
|
|
65
|
+
the model field whose boundary payload it contains. This allows a
|
|
66
|
+
boundary builder to construct coupled BCs from the complete pre-BC state
|
|
67
|
+
while preserving an unambiguous field-to-BC association.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
face_bc: FciFieldBundle | None = None
|
|
71
|
+
cut_wall_bc: FciFieldBundle | None = None
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
if self.face_bc is not None and not isinstance(self.face_bc, FciFieldBundle):
|
|
75
|
+
raise TypeError("LocalBoundaryData3D.face_bc must be an FciFieldBundle or None")
|
|
76
|
+
if self.cut_wall_bc is not None and not isinstance(self.cut_wall_bc, FciFieldBundle):
|
|
77
|
+
raise TypeError(
|
|
78
|
+
"LocalBoundaryData3D.cut_wall_bc must be an FciFieldBundle or None"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def tree_flatten(self):
|
|
82
|
+
return ((self.face_bc, self.cut_wall_bc), None)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
86
|
+
face_bc, cut_wall_bc = children
|
|
87
|
+
return cls(face_bc=face_bc, cut_wall_bc=cut_wall_bc)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@_pytree_base
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class LocalBoundaryConditionBuilder(_DataclassPyTreeMixin):
|
|
93
|
+
"""Build all field boundary payloads from a complete pre-BC state."""
|
|
94
|
+
|
|
95
|
+
build_fn: Callable[
|
|
96
|
+
[
|
|
97
|
+
FciModelState,
|
|
98
|
+
LocalFciGeometry3D,
|
|
99
|
+
LocalDomain3D,
|
|
100
|
+
LocalCutWallGeometry3D | None,
|
|
101
|
+
],
|
|
102
|
+
LocalBoundaryData3D,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def __call__(
|
|
106
|
+
self,
|
|
107
|
+
state_halo_pre_bc: FciModelState,
|
|
108
|
+
geometry: LocalFciGeometry3D,
|
|
109
|
+
domain: LocalDomain3D,
|
|
110
|
+
cut_wall_geometry: LocalCutWallGeometry3D | None,
|
|
111
|
+
) -> LocalBoundaryData3D:
|
|
112
|
+
if not isinstance(state_halo_pre_bc, FciModelState):
|
|
113
|
+
raise TypeError(
|
|
114
|
+
"LocalBoundaryConditionBuilder requires an FciModelState "
|
|
115
|
+
"with physical ghost cells not yet filled"
|
|
116
|
+
)
|
|
117
|
+
state_halo_pre_bc.assert_field_shape(domain.layout.cell_halo_shape)
|
|
118
|
+
result = self.build_fn(
|
|
119
|
+
state_halo_pre_bc,
|
|
120
|
+
geometry,
|
|
121
|
+
domain,
|
|
122
|
+
cut_wall_geometry,
|
|
123
|
+
)
|
|
124
|
+
if not isinstance(result, LocalBoundaryData3D):
|
|
125
|
+
raise TypeError(
|
|
126
|
+
"LocalBoundaryConditionBuilder.build_fn must return "
|
|
127
|
+
"LocalBoundaryData3D"
|
|
128
|
+
)
|
|
129
|
+
if result.face_bc is not None:
|
|
130
|
+
assert_matching_field_names(state_halo_pre_bc, result.face_bc)
|
|
131
|
+
if result.cut_wall_bc is not None:
|
|
132
|
+
assert_matching_field_names(state_halo_pre_bc, result.cut_wall_bc)
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
def tree_flatten(self):
|
|
136
|
+
return (), self.build_fn
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def tree_unflatten(cls, aux_data, children):
|
|
140
|
+
return cls(aux_data)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@_pytree_base
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class CoordinateFaceValueReconstructor3D:
|
|
147
|
+
"""Dense coordinate-face value reconstructor for cell-centered fields."""
|
|
148
|
+
|
|
149
|
+
def extrapolate(
|
|
150
|
+
self,
|
|
151
|
+
field: jnp.ndarray,
|
|
152
|
+
geometry: FciGeometry3D,
|
|
153
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
154
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False),
|
|
155
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
156
|
+
values = _as_float64_array(field, "CoordinateFaceValueReconstructor3D.field")
|
|
157
|
+
if values.shape != geometry.shape:
|
|
158
|
+
raise ValueError(f"field must have shape {geometry.shape}, got {values.shape}")
|
|
159
|
+
periodic_axes = _normalize_axis_flags(periodic_axes, "periodic_axes")
|
|
160
|
+
axis_regular_axes = _normalize_axis_flags(axis_regular_axes, "axis_regular_axes")
|
|
161
|
+
if any(periodic and axis_regular for periodic, axis_regular in zip(periodic_axes, axis_regular_axes)):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"periodic_axes and axis_regular_axes cannot both be True on the same axis; "
|
|
164
|
+
f"got periodic_axes={periodic_axes}, axis_regular_axes={axis_regular_axes}"
|
|
165
|
+
)
|
|
166
|
+
if axis_regular_axes[1] or axis_regular_axes[2]:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"axis_regular_axes currently only supports the lower x axis; "
|
|
169
|
+
f"got axis_regular_axes={axis_regular_axes}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
x_faces = jnp.empty((values.shape[0] + 1, values.shape[1], values.shape[2]), dtype=jnp.float64)
|
|
173
|
+
y_faces = jnp.empty((values.shape[0], values.shape[1] + 1, values.shape[2]), dtype=jnp.float64)
|
|
174
|
+
z_faces = jnp.empty((values.shape[0], values.shape[1], values.shape[2] + 1), dtype=jnp.float64)
|
|
175
|
+
|
|
176
|
+
x_faces = x_faces.at[1:-1].set(0.5 * (values[:-1] + values[1:]))
|
|
177
|
+
if periodic_axes[0]:
|
|
178
|
+
x_periodic = 0.5 * (values[0] + values[-1])
|
|
179
|
+
x_faces = x_faces.at[0].set(x_periodic)
|
|
180
|
+
x_faces = x_faces.at[-1].set(x_periodic)
|
|
181
|
+
elif axis_regular_axes[0]:
|
|
182
|
+
x_faces = x_faces.at[0].set(_axis_regular_lower_x_face(values))
|
|
183
|
+
x_faces = x_faces.at[-1].set(values[-1])
|
|
184
|
+
else:
|
|
185
|
+
x_faces = x_faces.at[0].set(values[0])
|
|
186
|
+
x_faces = x_faces.at[-1].set(values[-1])
|
|
187
|
+
|
|
188
|
+
y_faces = y_faces.at[:, 1:-1, :].set(0.5 * (values[:, :-1, :] + values[:, 1:, :]))
|
|
189
|
+
if periodic_axes[1]:
|
|
190
|
+
y_periodic = 0.5 * (values[:, 0, :] + values[:, -1, :])
|
|
191
|
+
y_faces = y_faces.at[:, 0, :].set(y_periodic)
|
|
192
|
+
y_faces = y_faces.at[:, -1, :].set(y_periodic)
|
|
193
|
+
else:
|
|
194
|
+
y_faces = y_faces.at[:, 0, :].set(values[:, 0, :])
|
|
195
|
+
y_faces = y_faces.at[:, -1, :].set(values[:, -1, :])
|
|
196
|
+
|
|
197
|
+
z_faces = z_faces.at[:, :, 1:-1].set(0.5 * (values[:, :, :-1] + values[:, :, 1:]))
|
|
198
|
+
if periodic_axes[2]:
|
|
199
|
+
z_periodic = 0.5 * (values[:, :, 0] + values[:, :, -1])
|
|
200
|
+
z_faces = z_faces.at[:, :, 0].set(z_periodic)
|
|
201
|
+
z_faces = z_faces.at[:, :, -1].set(z_periodic)
|
|
202
|
+
else:
|
|
203
|
+
z_faces = z_faces.at[:, :, 0].set(values[:, :, 0])
|
|
204
|
+
z_faces = z_faces.at[:, :, -1].set(values[:, :, -1])
|
|
205
|
+
return x_faces, y_faces, z_faces
|
|
206
|
+
|
|
207
|
+
def extrapolate_neumann(
|
|
208
|
+
self,
|
|
209
|
+
field: jnp.ndarray,
|
|
210
|
+
geometry: FciGeometry3D,
|
|
211
|
+
normal_derivative: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
212
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
213
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False),
|
|
214
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
215
|
+
values = _as_float64_array(field, "CoordinateFaceValueReconstructor3D.field")
|
|
216
|
+
if values.shape != geometry.shape:
|
|
217
|
+
raise ValueError(f"field must have shape {geometry.shape}, got {values.shape}")
|
|
218
|
+
periodic_axes = _normalize_axis_flags(periodic_axes, "periodic_axes")
|
|
219
|
+
axis_regular_axes = _normalize_axis_flags(axis_regular_axes, "axis_regular_axes")
|
|
220
|
+
if any(periodic and axis_regular for periodic, axis_regular in zip(periodic_axes, axis_regular_axes)):
|
|
221
|
+
raise ValueError(
|
|
222
|
+
"periodic_axes and axis_regular_axes cannot both be True on the same axis; "
|
|
223
|
+
f"got periodic_axes={periodic_axes}, axis_regular_axes={axis_regular_axes}"
|
|
224
|
+
)
|
|
225
|
+
if axis_regular_axes[1] or axis_regular_axes[2]:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"axis_regular_axes currently only supports the lower x axis; "
|
|
228
|
+
f"got axis_regular_axes={axis_regular_axes}"
|
|
229
|
+
)
|
|
230
|
+
# normal_derivative is interpreted as the outward-pointing normal derivative
|
|
231
|
+
# on the nonperiodic boundary faces. The lower x face is topological when
|
|
232
|
+
# axis_regular_axes[0] is True, so it is left untouched.
|
|
233
|
+
gx, gy, gz = _as_coordinate_face_tuple(
|
|
234
|
+
normal_derivative,
|
|
235
|
+
geometry,
|
|
236
|
+
"CoordinateFaceValueReconstructor3D.normal_derivative",
|
|
237
|
+
)
|
|
238
|
+
x_faces, y_faces, z_faces = self.extrapolate(
|
|
239
|
+
values,
|
|
240
|
+
geometry,
|
|
241
|
+
periodic_axes=periodic_axes,
|
|
242
|
+
axis_regular_axes=axis_regular_axes,
|
|
243
|
+
)
|
|
244
|
+
dx = jnp.asarray(geometry.spacing.dx, dtype=jnp.float64)
|
|
245
|
+
dy = jnp.asarray(geometry.spacing.dy, dtype=jnp.float64)
|
|
246
|
+
dz = jnp.asarray(geometry.spacing.dz, dtype=jnp.float64)
|
|
247
|
+
|
|
248
|
+
if not periodic_axes[0] and not axis_regular_axes[0]:
|
|
249
|
+
x_faces = x_faces.at[0].set(values[0] + 0.5 * dx[0] * gx[0])
|
|
250
|
+
x_faces = x_faces.at[-1].set(values[-1] + 0.5 * dx[-1] * gx[-1])
|
|
251
|
+
elif not periodic_axes[0]:
|
|
252
|
+
x_faces = x_faces.at[-1].set(values[-1] + 0.5 * dx[-1] * gx[-1])
|
|
253
|
+
if not periodic_axes[1]:
|
|
254
|
+
y_faces = y_faces.at[:, 0, :].set(values[:, 0, :] + 0.5 * dy[:, 0, :] * gy[:, 0, :])
|
|
255
|
+
y_faces = y_faces.at[:, -1, :].set(values[:, -1, :] + 0.5 * dy[:, -1, :] * gy[:, -1, :])
|
|
256
|
+
if not periodic_axes[2]:
|
|
257
|
+
z_faces = z_faces.at[:, :, 0].set(values[:, :, 0] + 0.5 * dz[:, :, 0] * gz[:, :, 0])
|
|
258
|
+
z_faces = z_faces.at[:, :, -1].set(values[:, :, -1] + 0.5 * dz[:, :, -1] * gz[:, :, -1])
|
|
259
|
+
return x_faces, y_faces, z_faces
|
|
260
|
+
|
|
261
|
+
def tree_flatten(self):
|
|
262
|
+
return (), None
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
266
|
+
return cls()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _local_coordinate_face_values_from_halo(
|
|
270
|
+
field_halo: jnp.ndarray,
|
|
271
|
+
geometry: LocalFciGeometry3D,
|
|
272
|
+
layout: HaloLayout3D,
|
|
273
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
274
|
+
values = _as_float64_array(field_halo, "LocalCoordinateFaceValueReconstructor3D.field_halo")
|
|
275
|
+
if values.shape != layout.cell_halo_shape:
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"field_halo must have shape layout.cell_halo_shape; "
|
|
278
|
+
f"got {values.shape}, expected {layout.cell_halo_shape}"
|
|
279
|
+
)
|
|
280
|
+
if geometry.layout != layout:
|
|
281
|
+
raise ValueError("geometry and layout must share the same HaloLayout3D")
|
|
282
|
+
if layout.halo_width < 1:
|
|
283
|
+
raise ValueError("local face reconstruction requires halo_width >= 1")
|
|
284
|
+
|
|
285
|
+
h = layout.halo_width
|
|
286
|
+
nx, ny, nz = layout.owned_shape
|
|
287
|
+
|
|
288
|
+
x_faces = 0.5 * (
|
|
289
|
+
values[h - 1 : h + nx, h : h + ny, h : h + nz]
|
|
290
|
+
+ values[h : h + nx + 1, h : h + ny, h : h + nz]
|
|
291
|
+
)
|
|
292
|
+
y_faces = 0.5 * (
|
|
293
|
+
values[h : h + nx, h - 1 : h + ny, h : h + nz]
|
|
294
|
+
+ values[h : h + nx, h : h + ny + 1, h : h + nz]
|
|
295
|
+
)
|
|
296
|
+
z_faces = 0.5 * (
|
|
297
|
+
values[h : h + nx, h : h + ny, h - 1 : h + nz]
|
|
298
|
+
+ values[h : h + nx, h : h + ny, h : h + nz + 1]
|
|
299
|
+
)
|
|
300
|
+
return x_faces, y_faces, z_faces
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@_pytree_base
|
|
304
|
+
@dataclass(frozen=True)
|
|
305
|
+
class LocalCoordinateFaceValueReconstructor3D(_DataclassPyTreeMixin):
|
|
306
|
+
"""Reconstruct coordinate control-face values from a complete local halo field."""
|
|
307
|
+
|
|
308
|
+
reconstruct_fn: Callable[
|
|
309
|
+
[
|
|
310
|
+
jnp.ndarray,
|
|
311
|
+
"LocalFciGeometry3D",
|
|
312
|
+
"HaloLayout3D",
|
|
313
|
+
],
|
|
314
|
+
tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
315
|
+
] = _local_coordinate_face_values_from_halo
|
|
316
|
+
|
|
317
|
+
def extrapolate(
|
|
318
|
+
self,
|
|
319
|
+
field_halo: jnp.ndarray,
|
|
320
|
+
geometry: "LocalFciGeometry3D",
|
|
321
|
+
layout: "HaloLayout3D",
|
|
322
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
323
|
+
return self.reconstruct_fn(field_halo, geometry, layout)
|
|
324
|
+
|
|
325
|
+
def tree_flatten(self):
|
|
326
|
+
return (), self.reconstruct_fn
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def tree_unflatten(cls, aux_data, children):
|
|
330
|
+
return cls(aux_data)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@_pytree_base
|
|
334
|
+
@dataclass(frozen=True)
|
|
335
|
+
class LocalCoordinateSideValues1D(_DataclassPyTreeMixin):
|
|
336
|
+
"""Lower/upper side-plane payloads for one coordinate axis."""
|
|
337
|
+
|
|
338
|
+
lower: jnp.ndarray
|
|
339
|
+
upper: jnp.ndarray
|
|
340
|
+
mask_lower: jnp.ndarray
|
|
341
|
+
mask_upper: jnp.ndarray
|
|
342
|
+
|
|
343
|
+
def __post_init__(self) -> None:
|
|
344
|
+
lower = jnp.asarray(self.lower, dtype=jnp.float64)
|
|
345
|
+
upper = jnp.asarray(self.upper, dtype=jnp.float64)
|
|
346
|
+
mask_lower = jnp.asarray(self.mask_lower, dtype=bool)
|
|
347
|
+
mask_upper = jnp.asarray(self.mask_upper, dtype=bool)
|
|
348
|
+
if lower.shape != upper.shape:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
"LocalCoordinateSideValues1D.lower and upper must have the same shape; "
|
|
351
|
+
f"got lower={lower.shape}, upper={upper.shape}"
|
|
352
|
+
)
|
|
353
|
+
if mask_lower.shape != lower.shape or mask_upper.shape != lower.shape:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
"LocalCoordinateSideValues1D masks must match the side-plane shape; "
|
|
356
|
+
f"got lower={lower.shape}, mask_lower={mask_lower.shape}, mask_upper={mask_upper.shape}"
|
|
357
|
+
)
|
|
358
|
+
object.__setattr__(self, "lower", lower)
|
|
359
|
+
object.__setattr__(self, "upper", upper)
|
|
360
|
+
object.__setattr__(self, "mask_lower", mask_lower)
|
|
361
|
+
object.__setattr__(self, "mask_upper", mask_upper)
|
|
362
|
+
|
|
363
|
+
def replace(self, **updates: object) -> "LocalCoordinateSideValues1D":
|
|
364
|
+
allowed = {"lower", "upper", "mask_lower", "mask_upper"}
|
|
365
|
+
unknown = set(updates) - allowed
|
|
366
|
+
if unknown:
|
|
367
|
+
names = ", ".join(sorted(unknown))
|
|
368
|
+
raise ValueError(f"Unknown LocalCoordinateSideValues1D field(s): {names}")
|
|
369
|
+
return LocalCoordinateSideValues1D(
|
|
370
|
+
lower=updates.get("lower", self.lower),
|
|
371
|
+
upper=updates.get("upper", self.upper),
|
|
372
|
+
mask_lower=updates.get("mask_lower", self.mask_lower),
|
|
373
|
+
mask_upper=updates.get("mask_upper", self.mask_upper),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def tree_flatten(self):
|
|
377
|
+
return ((self.lower, self.upper, self.mask_lower, self.mask_upper), None)
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
381
|
+
return cls(*children)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@_pytree_base
|
|
385
|
+
@dataclass(frozen=True)
|
|
386
|
+
class LocalCoordinateSideValues3D(_DataclassPyTreeMixin):
|
|
387
|
+
"""Lower/upper coordinate-side payloads for the three axes."""
|
|
388
|
+
|
|
389
|
+
x: LocalCoordinateSideValues1D
|
|
390
|
+
y: LocalCoordinateSideValues1D
|
|
391
|
+
z: LocalCoordinateSideValues1D
|
|
392
|
+
|
|
393
|
+
def __post_init__(self) -> None:
|
|
394
|
+
if not isinstance(self.x, LocalCoordinateSideValues1D):
|
|
395
|
+
raise TypeError("LocalCoordinateSideValues3D.x must be a LocalCoordinateSideValues1D")
|
|
396
|
+
if not isinstance(self.y, LocalCoordinateSideValues1D):
|
|
397
|
+
raise TypeError("LocalCoordinateSideValues3D.y must be a LocalCoordinateSideValues1D")
|
|
398
|
+
if not isinstance(self.z, LocalCoordinateSideValues1D):
|
|
399
|
+
raise TypeError("LocalCoordinateSideValues3D.z must be a LocalCoordinateSideValues1D")
|
|
400
|
+
|
|
401
|
+
def replace(self, **updates: object) -> "LocalCoordinateSideValues3D":
|
|
402
|
+
allowed = {"x", "y", "z"}
|
|
403
|
+
unknown = set(updates) - allowed
|
|
404
|
+
if unknown:
|
|
405
|
+
names = ", ".join(sorted(unknown))
|
|
406
|
+
raise ValueError(f"Unknown LocalCoordinateSideValues3D field(s): {names}")
|
|
407
|
+
return LocalCoordinateSideValues3D(
|
|
408
|
+
x=updates.get("x", self.x),
|
|
409
|
+
y=updates.get("y", self.y),
|
|
410
|
+
z=updates.get("z", self.z),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def tree_flatten(self):
|
|
414
|
+
return ((self.x, self.y, self.z), None)
|
|
415
|
+
|
|
416
|
+
@classmethod
|
|
417
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
418
|
+
return cls(*children)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _local_side_samples(
|
|
422
|
+
field_halo: jnp.ndarray,
|
|
423
|
+
layout: HaloLayout3D,
|
|
424
|
+
axis: int,
|
|
425
|
+
side: int,
|
|
426
|
+
sample_count: int,
|
|
427
|
+
) -> list[jnp.ndarray]:
|
|
428
|
+
values = _local_cell_halo_array(field_halo, layout, "field_halo")
|
|
429
|
+
axis = int(axis)
|
|
430
|
+
side = int(side)
|
|
431
|
+
if sample_count < 1:
|
|
432
|
+
raise ValueError(f"sample_count must be positive, got {sample_count}")
|
|
433
|
+
h = layout.halo_width
|
|
434
|
+
nx, ny, nz = layout.owned_shape
|
|
435
|
+
if axis == 0:
|
|
436
|
+
base = h if side == 0 else h + nx - 1
|
|
437
|
+
step = 1 if side == 0 else -1
|
|
438
|
+
return [values[base + step * i, h : h + ny, h : h + nz] for i in range(sample_count)]
|
|
439
|
+
if axis == 1:
|
|
440
|
+
base = h if side == 0 else h + ny - 1
|
|
441
|
+
step = 1 if side == 0 else -1
|
|
442
|
+
return [values[h : h + nx, base + step * i, h : h + nz] for i in range(sample_count)]
|
|
443
|
+
base = h if side == 0 else h + nz - 1
|
|
444
|
+
step = 1 if side == 0 else -1
|
|
445
|
+
return [values[h : h + nx, h : h + ny, base + step * i] for i in range(sample_count)]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _local_coordinate_side_values_from_array(
|
|
449
|
+
values: jnp.ndarray,
|
|
450
|
+
geometry: LocalFciGeometry3D,
|
|
451
|
+
layout: HaloLayout3D,
|
|
452
|
+
*,
|
|
453
|
+
name: str,
|
|
454
|
+
) -> LocalCoordinateSideValues3D:
|
|
455
|
+
values = _as_float64_array(values, name)
|
|
456
|
+
if values.shape != layout.cell_halo_shape:
|
|
457
|
+
raise ValueError(f"{name} must have shape {layout.cell_halo_shape}, got {values.shape}")
|
|
458
|
+
if geometry.layout != layout:
|
|
459
|
+
raise ValueError("geometry and layout must share the same HaloLayout3D")
|
|
460
|
+
nx, ny, nz = layout.owned_shape
|
|
461
|
+
h = layout.halo_width
|
|
462
|
+
return LocalCoordinateSideValues3D(
|
|
463
|
+
x=LocalCoordinateSideValues1D(
|
|
464
|
+
lower=values[h - 1 : h, h : h + ny, h : h + nz][0],
|
|
465
|
+
upper=values[h + nx : h + nx + 1, h : h + ny, h : h + nz][0],
|
|
466
|
+
mask_lower=jnp.ones((ny, nz), dtype=bool),
|
|
467
|
+
mask_upper=jnp.ones((ny, nz), dtype=bool),
|
|
468
|
+
),
|
|
469
|
+
y=LocalCoordinateSideValues1D(
|
|
470
|
+
lower=values[h : h + nx, h - 1 : h, h : h + nz][:, 0, :],
|
|
471
|
+
upper=values[h : h + nx, h + ny : h + ny + 1, h : h + nz][:, 0, :],
|
|
472
|
+
mask_lower=jnp.ones((nx, nz), dtype=bool),
|
|
473
|
+
mask_upper=jnp.ones((nx, nz), dtype=bool),
|
|
474
|
+
),
|
|
475
|
+
z=LocalCoordinateSideValues1D(
|
|
476
|
+
lower=values[h : h + nx, h : h + ny, h - 1 : h][:, :, 0],
|
|
477
|
+
upper=values[h : h + nx, h : h + ny, h + nz : h + nz + 1][:, :, 0],
|
|
478
|
+
mask_lower=jnp.ones((nx, ny), dtype=bool),
|
|
479
|
+
mask_upper=jnp.ones((nx, ny), dtype=bool),
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@_pytree_base
|
|
485
|
+
@dataclass(frozen=True)
|
|
486
|
+
class LocalCoordinateNormalDerivativeConstructor3D(_DataclassPyTreeMixin):
|
|
487
|
+
"""Construct coordinate-side normal derivatives using local halo data."""
|
|
488
|
+
|
|
489
|
+
dnormal_weights: jnp.ndarray
|
|
490
|
+
d2normal_weights: jnp.ndarray
|
|
491
|
+
|
|
492
|
+
def __post_init__(self) -> None:
|
|
493
|
+
dnormal_weights = jnp.asarray(self.dnormal_weights, dtype=jnp.float64)
|
|
494
|
+
d2normal_weights = jnp.asarray(self.d2normal_weights, dtype=jnp.float64)
|
|
495
|
+
if dnormal_weights.ndim != 3 or d2normal_weights.ndim != 3:
|
|
496
|
+
raise ValueError(
|
|
497
|
+
"LocalCoordinateNormalDerivativeConstructor3D weights must have shape (3, 2, stencil_width)"
|
|
498
|
+
)
|
|
499
|
+
if dnormal_weights.shape != d2normal_weights.shape:
|
|
500
|
+
raise ValueError(
|
|
501
|
+
"LocalCoordinateNormalDerivativeConstructor3D weight tensors must have the same shape; "
|
|
502
|
+
f"got {dnormal_weights.shape} and {d2normal_weights.shape}"
|
|
503
|
+
)
|
|
504
|
+
if dnormal_weights.shape[0] != 3 or dnormal_weights.shape[1] != 2:
|
|
505
|
+
raise ValueError(
|
|
506
|
+
"LocalCoordinateNormalDerivativeConstructor3D weights must have leading shape (3, 2, stencil_width); "
|
|
507
|
+
f"got {dnormal_weights.shape}"
|
|
508
|
+
)
|
|
509
|
+
if dnormal_weights.shape[2] < 2:
|
|
510
|
+
raise ValueError(
|
|
511
|
+
"LocalCoordinateNormalDerivativeConstructor3D requires at least two stencil points"
|
|
512
|
+
)
|
|
513
|
+
object.__setattr__(self, "dnormal_weights", dnormal_weights)
|
|
514
|
+
object.__setattr__(self, "d2normal_weights", d2normal_weights)
|
|
515
|
+
|
|
516
|
+
@property
|
|
517
|
+
def stencil_width(self) -> int:
|
|
518
|
+
return int(self.dnormal_weights.shape[2])
|
|
519
|
+
|
|
520
|
+
@classmethod
|
|
521
|
+
def from_geometry(cls, geometry: FciGeometry3D) -> "LocalCoordinateNormalDerivativeConstructor3D":
|
|
522
|
+
base = CoordinateNormalDerivativeConstructor3D.from_geometry(geometry)
|
|
523
|
+
return cls(dnormal_weights=base.dnormal_weights, d2normal_weights=base.d2normal_weights)
|
|
524
|
+
|
|
525
|
+
def _wall_side_derivatives(
|
|
526
|
+
self,
|
|
527
|
+
field_halo: jnp.ndarray,
|
|
528
|
+
wall_value: LocalCoordinateSideValues1D,
|
|
529
|
+
geometry: LocalFciGeometry3D,
|
|
530
|
+
layout: HaloLayout3D,
|
|
531
|
+
*,
|
|
532
|
+
axis: int,
|
|
533
|
+
) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
534
|
+
sample_count = self.stencil_width - 1
|
|
535
|
+
lower_samples = _local_side_samples(field_halo, layout, axis, 0, sample_count)
|
|
536
|
+
upper_samples = _local_side_samples(field_halo, layout, axis, 1, sample_count)
|
|
537
|
+
dnormal_lower = self.dnormal_weights[axis, 0, 0] * wall_value.lower
|
|
538
|
+
d2normal_lower = self.d2normal_weights[axis, 0, 0] * wall_value.lower
|
|
539
|
+
dnormal_upper = self.dnormal_weights[axis, 1, 0] * wall_value.upper
|
|
540
|
+
d2normal_upper = self.d2normal_weights[axis, 1, 0] * wall_value.upper
|
|
541
|
+
for idx, sample in enumerate(lower_samples, start=1):
|
|
542
|
+
dnormal_lower = dnormal_lower + self.dnormal_weights[axis, 0, idx] * sample
|
|
543
|
+
d2normal_lower = d2normal_lower + self.d2normal_weights[axis, 0, idx] * sample
|
|
544
|
+
for idx, sample in enumerate(upper_samples, start=1):
|
|
545
|
+
dnormal_upper = dnormal_upper + self.dnormal_weights[axis, 1, idx] * sample
|
|
546
|
+
d2normal_upper = d2normal_upper + self.d2normal_weights[axis, 1, idx] * sample
|
|
547
|
+
dnormal_lower = jnp.where(wall_value.mask_lower, dnormal_lower, 0.0)
|
|
548
|
+
d2normal_lower = jnp.where(wall_value.mask_lower, d2normal_lower, 0.0)
|
|
549
|
+
dnormal_upper = jnp.where(wall_value.mask_upper, dnormal_upper, 0.0)
|
|
550
|
+
d2normal_upper = jnp.where(wall_value.mask_upper, d2normal_upper, 0.0)
|
|
551
|
+
return dnormal_lower, dnormal_upper, d2normal_lower, d2normal_upper
|
|
552
|
+
|
|
553
|
+
def dnormal_from_wall_value(
|
|
554
|
+
self,
|
|
555
|
+
field_halo: jnp.ndarray,
|
|
556
|
+
wall_value: LocalCoordinateSideValues3D,
|
|
557
|
+
geometry: "LocalFciGeometry3D",
|
|
558
|
+
layout: "HaloLayout3D",
|
|
559
|
+
) -> "LocalCoordinateSideValues3D":
|
|
560
|
+
dnormal, _d2normal = self.normal_derivatives_from_wall_value(field_halo, wall_value, geometry, layout)
|
|
561
|
+
return dnormal
|
|
562
|
+
|
|
563
|
+
def d2normal_from_wall_value(
|
|
564
|
+
self,
|
|
565
|
+
field_halo: jnp.ndarray,
|
|
566
|
+
wall_value: LocalCoordinateSideValues3D,
|
|
567
|
+
geometry: "LocalFciGeometry3D",
|
|
568
|
+
layout: "HaloLayout3D",
|
|
569
|
+
) -> "LocalCoordinateSideValues3D":
|
|
570
|
+
_dnormal, d2normal = self.normal_derivatives_from_wall_value(field_halo, wall_value, geometry, layout)
|
|
571
|
+
return d2normal
|
|
572
|
+
|
|
573
|
+
def normal_derivatives_from_wall_value(
|
|
574
|
+
self,
|
|
575
|
+
field_halo: jnp.ndarray,
|
|
576
|
+
wall_value: LocalCoordinateSideValues3D,
|
|
577
|
+
geometry: "LocalFciGeometry3D",
|
|
578
|
+
layout: "HaloLayout3D",
|
|
579
|
+
) -> tuple["LocalCoordinateSideValues3D", "LocalCoordinateSideValues3D"]:
|
|
580
|
+
values = _local_cell_halo_array(field_halo, layout, "field_halo")
|
|
581
|
+
if geometry.layout != layout:
|
|
582
|
+
raise ValueError("geometry and layout must share the same HaloLayout3D")
|
|
583
|
+
if layout.halo_width < 1:
|
|
584
|
+
raise ValueError("local normal-derivative reconstruction requires halo_width >= 1")
|
|
585
|
+
if not isinstance(wall_value, LocalCoordinateSideValues3D):
|
|
586
|
+
raise TypeError("wall_value must be a LocalCoordinateSideValues3D instance")
|
|
587
|
+
|
|
588
|
+
x_dnormal_lower, x_dnormal_upper, x_d2_lower, x_d2_upper = self._wall_side_derivatives(
|
|
589
|
+
values, wall_value.x, geometry, layout, axis=0
|
|
590
|
+
)
|
|
591
|
+
y_dnormal_lower, y_dnormal_upper, y_d2_lower, y_d2_upper = self._wall_side_derivatives(
|
|
592
|
+
values, wall_value.y, geometry, layout, axis=1
|
|
593
|
+
)
|
|
594
|
+
z_dnormal_lower, z_dnormal_upper, z_d2_lower, z_d2_upper = self._wall_side_derivatives(
|
|
595
|
+
values, wall_value.z, geometry, layout, axis=2
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return (
|
|
599
|
+
LocalCoordinateSideValues3D(
|
|
600
|
+
x=LocalCoordinateSideValues1D(
|
|
601
|
+
lower=x_dnormal_lower,
|
|
602
|
+
upper=x_dnormal_upper,
|
|
603
|
+
mask_lower=wall_value.x.mask_lower,
|
|
604
|
+
mask_upper=wall_value.x.mask_upper,
|
|
605
|
+
),
|
|
606
|
+
y=LocalCoordinateSideValues1D(
|
|
607
|
+
lower=y_dnormal_lower,
|
|
608
|
+
upper=y_dnormal_upper,
|
|
609
|
+
mask_lower=wall_value.y.mask_lower,
|
|
610
|
+
mask_upper=wall_value.y.mask_upper,
|
|
611
|
+
),
|
|
612
|
+
z=LocalCoordinateSideValues1D(
|
|
613
|
+
lower=z_dnormal_lower,
|
|
614
|
+
upper=z_dnormal_upper,
|
|
615
|
+
mask_lower=wall_value.z.mask_lower,
|
|
616
|
+
mask_upper=wall_value.z.mask_upper,
|
|
617
|
+
),
|
|
618
|
+
),
|
|
619
|
+
LocalCoordinateSideValues3D(
|
|
620
|
+
x=LocalCoordinateSideValues1D(
|
|
621
|
+
lower=x_d2_lower,
|
|
622
|
+
upper=x_d2_upper,
|
|
623
|
+
mask_lower=wall_value.x.mask_lower,
|
|
624
|
+
mask_upper=wall_value.x.mask_upper,
|
|
625
|
+
),
|
|
626
|
+
y=LocalCoordinateSideValues1D(
|
|
627
|
+
lower=y_d2_lower,
|
|
628
|
+
upper=y_d2_upper,
|
|
629
|
+
mask_lower=wall_value.y.mask_lower,
|
|
630
|
+
mask_upper=wall_value.y.mask_upper,
|
|
631
|
+
),
|
|
632
|
+
z=LocalCoordinateSideValues1D(
|
|
633
|
+
lower=z_d2_lower,
|
|
634
|
+
upper=z_d2_upper,
|
|
635
|
+
mask_lower=wall_value.z.mask_lower,
|
|
636
|
+
mask_upper=wall_value.z.mask_upper,
|
|
637
|
+
),
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
def tree_flatten(self):
|
|
642
|
+
return ((self.dnormal_weights, self.d2normal_weights), None)
|
|
643
|
+
|
|
644
|
+
@classmethod
|
|
645
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
646
|
+
return cls(*children)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@_pytree_base
|
|
650
|
+
@dataclass(frozen=True)
|
|
651
|
+
class CoordinateNormalDerivativeConstructor3D:
|
|
652
|
+
"""Dense coordinate-boundary normal derivative constructor.
|
|
653
|
+
|
|
654
|
+
The stored weights have shape ``(3, 2, 4)``:
|
|
655
|
+
axis 0 is x/y/z, axis 1 is lower/upper wall, and axis 2 multiplies
|
|
656
|
+
``(wall_value, first interior cell, second interior cell, third interior cell)``.
|
|
657
|
+
These weights are the coordinate-face analogue of the cut-wall least-squares
|
|
658
|
+
weights below: construction is geometry-dependent, application is just a
|
|
659
|
+
linear combination of wall and cell-centered values.
|
|
660
|
+
|
|
661
|
+
Sign convention:
|
|
662
|
+
dnormal_from_wall_value(...) returns the outward normal derivative
|
|
663
|
+
d/dn, not the inward coordinate derivative d/ds. The weight factory
|
|
664
|
+
applies the sign flip once during construction so callers can use the
|
|
665
|
+
physical outward-normal convention directly.
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
dnormal_weights: jnp.ndarray # (axis=3, side=2, wall+3 interior nodes=4)
|
|
669
|
+
d2normal_weights: jnp.ndarray # (axis=3, side=2, wall+3 interior nodes=4)
|
|
670
|
+
|
|
671
|
+
def __post_init__(self) -> None:
|
|
672
|
+
object.__setattr__(
|
|
673
|
+
self,
|
|
674
|
+
"dnormal_weights",
|
|
675
|
+
_as_coordinate_derivative_weight_array(
|
|
676
|
+
self.dnormal_weights,
|
|
677
|
+
"CoordinateNormalDerivativeConstructor3D.dnormal_weights",
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
object.__setattr__(
|
|
681
|
+
self,
|
|
682
|
+
"d2normal_weights",
|
|
683
|
+
_as_coordinate_derivative_weight_array(
|
|
684
|
+
self.d2normal_weights,
|
|
685
|
+
"CoordinateNormalDerivativeConstructor3D.d2normal_weights",
|
|
686
|
+
),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
@classmethod
|
|
690
|
+
def from_geometry(cls, geometry: FciGeometry3D) -> "CoordinateNormalDerivativeConstructor3D":
|
|
691
|
+
"""Build second-order wall-normal derivative weights from grid geometry."""
|
|
692
|
+
|
|
693
|
+
if geometry.shape[0] < 3 or geometry.shape[1] < 3 or geometry.shape[2] < 3:
|
|
694
|
+
raise ValueError(
|
|
695
|
+
"CoordinateNormalDerivativeConstructor3D.from_geometry requires at least "
|
|
696
|
+
f"three cells along every axis, got {geometry.shape}"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
def _weights_for_side(a, b, c):
|
|
700
|
+
nodes = jnp.asarray((0.0, a, b, c), dtype=jnp.float64)
|
|
701
|
+
vandermonde = jnp.stack((nodes**0, nodes, nodes**2, nodes**3), axis=0)
|
|
702
|
+
# Local coordinate s points inward from the wall towards computational domain.
|
|
703
|
+
#The returned first-derivative weights are already converted to the outward
|
|
704
|
+
# normal convention pointing away from the computational domain,
|
|
705
|
+
#so d/dn = -d/ds is baked in once here.
|
|
706
|
+
d1_weights = -jnp.linalg.solve(
|
|
707
|
+
vandermonde,
|
|
708
|
+
jnp.asarray((0.0, 1.0, 0.0, 0.0), dtype=jnp.float64),
|
|
709
|
+
)
|
|
710
|
+
# The second normal derivative has no sign flip:
|
|
711
|
+
# d2/dn2 = d2/ds2.
|
|
712
|
+
d2_weights = jnp.linalg.solve(
|
|
713
|
+
vandermonde,
|
|
714
|
+
jnp.asarray((0.0, 0.0, 2.0, 0.0), dtype=jnp.float64),
|
|
715
|
+
)
|
|
716
|
+
return d1_weights, d2_weights
|
|
717
|
+
|
|
718
|
+
def _axis_weights(axis_grid):
|
|
719
|
+
lower_a = jnp.asarray(axis_grid.centers[0] - axis_grid.faces[0], dtype=jnp.float64)
|
|
720
|
+
lower_b = jnp.asarray(axis_grid.centers[1] - axis_grid.faces[0], dtype=jnp.float64)
|
|
721
|
+
lower_c = jnp.asarray(axis_grid.centers[2] - axis_grid.faces[0], dtype=jnp.float64)
|
|
722
|
+
upper_a = jnp.asarray(axis_grid.faces[-1] - axis_grid.centers[-1], dtype=jnp.float64)
|
|
723
|
+
upper_b = jnp.asarray(axis_grid.faces[-1] - axis_grid.centers[-2], dtype=jnp.float64)
|
|
724
|
+
upper_c = jnp.asarray(axis_grid.faces[-1] - axis_grid.centers[-3], dtype=jnp.float64)
|
|
725
|
+
lower_d1, lower_d2 = _weights_for_side(lower_a, lower_b, lower_c)
|
|
726
|
+
upper_d1, upper_d2 = _weights_for_side(upper_a, upper_b, upper_c)
|
|
727
|
+
return jnp.stack((lower_d1, upper_d1), axis=0), jnp.stack((lower_d2, upper_d2), axis=0)
|
|
728
|
+
|
|
729
|
+
x_d1, x_d2 = _axis_weights(geometry.grid.x)
|
|
730
|
+
y_d1, y_d2 = _axis_weights(geometry.grid.y)
|
|
731
|
+
z_d1, z_d2 = _axis_weights(geometry.grid.z)
|
|
732
|
+
return cls(
|
|
733
|
+
dnormal_weights=jnp.stack((x_d1, y_d1, z_d1), axis=0),
|
|
734
|
+
d2normal_weights=jnp.stack((x_d2, y_d2, z_d2), axis=0),
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def dnormal_from_wall_value(
|
|
738
|
+
self,
|
|
739
|
+
field: jnp.ndarray,
|
|
740
|
+
wall_value: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
741
|
+
geometry: FciGeometry3D,
|
|
742
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
743
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False),
|
|
744
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
745
|
+
dnormal, _d2normal = self.normal_derivatives_from_wall_value(
|
|
746
|
+
field,
|
|
747
|
+
wall_value,
|
|
748
|
+
geometry,
|
|
749
|
+
periodic_axes=periodic_axes,
|
|
750
|
+
axis_regular_axes=axis_regular_axes,
|
|
751
|
+
)
|
|
752
|
+
return dnormal
|
|
753
|
+
|
|
754
|
+
def d2normal_from_wall_value(
|
|
755
|
+
self,
|
|
756
|
+
field: jnp.ndarray,
|
|
757
|
+
wall_value: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
758
|
+
geometry: FciGeometry3D,
|
|
759
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
760
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False),
|
|
761
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
762
|
+
_dnormal, d2normal = self.normal_derivatives_from_wall_value(
|
|
763
|
+
field,
|
|
764
|
+
wall_value,
|
|
765
|
+
geometry,
|
|
766
|
+
periodic_axes=periodic_axes,
|
|
767
|
+
axis_regular_axes=axis_regular_axes,
|
|
768
|
+
)
|
|
769
|
+
return d2normal
|
|
770
|
+
|
|
771
|
+
def normal_derivatives_from_wall_value(
|
|
772
|
+
self,
|
|
773
|
+
field: jnp.ndarray,
|
|
774
|
+
wall_value: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
775
|
+
geometry: FciGeometry3D,
|
|
776
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
777
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False),
|
|
778
|
+
) -> tuple[
|
|
779
|
+
tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
780
|
+
tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
781
|
+
]:
|
|
782
|
+
values = _as_float64_array(field, "CoordinateNormalDerivativeConstructor3D.field")
|
|
783
|
+
if values.shape != geometry.shape:
|
|
784
|
+
raise ValueError(f"field must have shape {geometry.shape}, got {values.shape}")
|
|
785
|
+
periodic_axes = _normalize_axis_flags(periodic_axes, "periodic_axes")
|
|
786
|
+
axis_regular_axes = _normalize_axis_flags(axis_regular_axes, "axis_regular_axes")
|
|
787
|
+
if any(periodic and axis_regular for periodic, axis_regular in zip(periodic_axes, axis_regular_axes)):
|
|
788
|
+
raise ValueError(
|
|
789
|
+
"periodic_axes and axis_regular_axes cannot both be True on the same axis; "
|
|
790
|
+
f"got periodic_axes={periodic_axes}, axis_regular_axes={axis_regular_axes}"
|
|
791
|
+
)
|
|
792
|
+
if axis_regular_axes[1] or axis_regular_axes[2]:
|
|
793
|
+
raise ValueError(
|
|
794
|
+
"axis_regular_axes currently only supports the lower x axis; "
|
|
795
|
+
f"got axis_regular_axes={axis_regular_axes}"
|
|
796
|
+
)
|
|
797
|
+
fw_x, fw_y, fw_z = _as_coordinate_face_tuple(
|
|
798
|
+
wall_value,
|
|
799
|
+
geometry,
|
|
800
|
+
"CoordinateNormalDerivativeConstructor3D.wall_value",
|
|
801
|
+
)
|
|
802
|
+
if axis_regular_axes[0]:
|
|
803
|
+
fw_x = fw_x.at[0].set(_axis_regular_lower_x_face(values))
|
|
804
|
+
|
|
805
|
+
dfn_x = jnp.zeros_like(fw_x, dtype=jnp.float64)
|
|
806
|
+
dfn_y = jnp.zeros_like(fw_y, dtype=jnp.float64)
|
|
807
|
+
dfn_z = jnp.zeros_like(fw_z, dtype=jnp.float64)
|
|
808
|
+
d2fn_x = jnp.zeros_like(fw_x, dtype=jnp.float64)
|
|
809
|
+
d2fn_y = jnp.zeros_like(fw_y, dtype=jnp.float64)
|
|
810
|
+
d2fn_z = jnp.zeros_like(fw_z, dtype=jnp.float64)
|
|
811
|
+
|
|
812
|
+
def _apply_weights(weights, f_wall, f0, f1, f2):
|
|
813
|
+
return weights[0] * f_wall + weights[1] * f0 + weights[2] * f1 + weights[3] * f2
|
|
814
|
+
|
|
815
|
+
def _wall_derivatives(axis: int, side: int, f_wall, f0, f1, f2):
|
|
816
|
+
dnormal = _apply_weights(self.dnormal_weights[axis, side], f_wall, f0, f1, f2)
|
|
817
|
+
d2normal = _apply_weights(self.d2normal_weights[axis, side], f_wall, f0, f1, f2)
|
|
818
|
+
return dnormal, d2normal
|
|
819
|
+
|
|
820
|
+
if not periodic_axes[0]:
|
|
821
|
+
if values.shape[0] < 3:
|
|
822
|
+
raise ValueError("x-normal derivative reconstruction requires at least three x cells")
|
|
823
|
+
lower_dfn, lower_d2fn = _wall_derivatives(0, 0, fw_x[0], values[0], values[1], values[2])
|
|
824
|
+
upper_dfn, upper_d2fn = _wall_derivatives(0, 1, fw_x[-1], values[-1], values[-2], values[-3])
|
|
825
|
+
dfn_x = dfn_x.at[0].set(lower_dfn)
|
|
826
|
+
dfn_x = dfn_x.at[-1].set(upper_dfn)
|
|
827
|
+
d2fn_x = d2fn_x.at[0].set(lower_d2fn)
|
|
828
|
+
d2fn_x = d2fn_x.at[-1].set(upper_d2fn)
|
|
829
|
+
|
|
830
|
+
if not periodic_axes[1]:
|
|
831
|
+
if values.shape[1] < 3:
|
|
832
|
+
raise ValueError("y-normal derivative reconstruction requires at least three y cells")
|
|
833
|
+
lower_dfn, lower_d2fn = _wall_derivatives(1, 0, fw_y[:, 0, :], values[:, 0, :], values[:, 1, :], values[:, 2, :])
|
|
834
|
+
upper_dfn, upper_d2fn = _wall_derivatives(1, 1, fw_y[:, -1, :], values[:, -1, :], values[:, -2, :], values[:, -3, :])
|
|
835
|
+
dfn_y = dfn_y.at[:, 0, :].set(lower_dfn)
|
|
836
|
+
dfn_y = dfn_y.at[:, -1, :].set(upper_dfn)
|
|
837
|
+
d2fn_y = d2fn_y.at[:, 0, :].set(lower_d2fn)
|
|
838
|
+
d2fn_y = d2fn_y.at[:, -1, :].set(upper_d2fn)
|
|
839
|
+
|
|
840
|
+
if not periodic_axes[2]:
|
|
841
|
+
if values.shape[2] < 3:
|
|
842
|
+
raise ValueError("z-normal derivative reconstruction requires at least three z cells")
|
|
843
|
+
lower_dfn, lower_d2fn = _wall_derivatives(2, 0, fw_z[:, :, 0], values[:, :, 0], values[:, :, 1], values[:, :, 2])
|
|
844
|
+
upper_dfn, upper_d2fn = _wall_derivatives(2, 1, fw_z[:, :, -1], values[:, :, -1], values[:, :, -2], values[:, :, -3])
|
|
845
|
+
dfn_z = dfn_z.at[:, :, 0].set(lower_dfn)
|
|
846
|
+
dfn_z = dfn_z.at[:, :, -1].set(upper_dfn)
|
|
847
|
+
d2fn_z = d2fn_z.at[:, :, 0].set(lower_d2fn)
|
|
848
|
+
d2fn_z = d2fn_z.at[:, :, -1].set(upper_d2fn)
|
|
849
|
+
|
|
850
|
+
return (dfn_x, dfn_y, dfn_z), (d2fn_x, d2fn_y, d2fn_z)
|
|
851
|
+
|
|
852
|
+
def tree_flatten(self):
|
|
853
|
+
return ((self.dnormal_weights, self.d2normal_weights), None)
|
|
854
|
+
|
|
855
|
+
@classmethod
|
|
856
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
857
|
+
return cls(*children)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
@_pytree_base
|
|
861
|
+
@dataclass(frozen=True)
|
|
862
|
+
class CutWallValueReconstructor3D:
|
|
863
|
+
"""Batched cut-wall value reconstructor for cell-centered fields."""
|
|
864
|
+
|
|
865
|
+
cut_wall_geometry: "CutWallGeometry3D" # geometry describing the cut-wall faces
|
|
866
|
+
neighbor_i: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
867
|
+
neighbor_j: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
868
|
+
neighbor_k: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
869
|
+
weights: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
870
|
+
bc_coeff: jnp.ndarray | None = None # (n_wall_faces,) or None for pure extrapolation
|
|
871
|
+
|
|
872
|
+
def __post_init__(self) -> None:
|
|
873
|
+
if not isinstance(self.cut_wall_geometry, CutWallGeometry3D):
|
|
874
|
+
raise TypeError("CutWallValueReconstructor3D.cut_wall_geometry must be a CutWallGeometry3D")
|
|
875
|
+
neighbor_i = _as_int_stencil_array(self.neighbor_i, "CutWallValueReconstructor3D.neighbor_i")
|
|
876
|
+
neighbor_j = _as_int_stencil_array(self.neighbor_j, "CutWallValueReconstructor3D.neighbor_j")
|
|
877
|
+
neighbor_k = _as_int_stencil_array(self.neighbor_k, "CutWallValueReconstructor3D.neighbor_k")
|
|
878
|
+
weights = _as_weight_stencil_array(self.weights, "CutWallValueReconstructor3D.weights")
|
|
879
|
+
if neighbor_i.shape != neighbor_j.shape or neighbor_i.shape != neighbor_k.shape or neighbor_i.shape != weights.shape:
|
|
880
|
+
raise ValueError("cut-wall neighbor and weight arrays must all have the same shape")
|
|
881
|
+
if neighbor_i.shape[0] != self.cut_wall_geometry.n_wall_faces:
|
|
882
|
+
raise ValueError(
|
|
883
|
+
"cut-wall neighbor arrays must have one row per cut-wall face; "
|
|
884
|
+
f"got {neighbor_i.shape[0]} rows and {self.cut_wall_geometry.n_wall_faces} faces"
|
|
885
|
+
)
|
|
886
|
+
object.__setattr__(self, "neighbor_i", neighbor_i)
|
|
887
|
+
object.__setattr__(self, "neighbor_j", neighbor_j)
|
|
888
|
+
object.__setattr__(self, "neighbor_k", neighbor_k)
|
|
889
|
+
object.__setattr__(self, "weights", weights)
|
|
890
|
+
if self.bc_coeff is not None:
|
|
891
|
+
bc_coeff = _as_wall_face_array(
|
|
892
|
+
self.bc_coeff,
|
|
893
|
+
self.cut_wall_geometry.n_wall_faces,
|
|
894
|
+
"CutWallValueReconstructor3D.bc_coeff",
|
|
895
|
+
)
|
|
896
|
+
object.__setattr__(self, "bc_coeff", bc_coeff)
|
|
897
|
+
|
|
898
|
+
@property
|
|
899
|
+
def n_wall_faces(self) -> int:
|
|
900
|
+
return self.cut_wall_geometry.n_wall_faces
|
|
901
|
+
|
|
902
|
+
def extrapolate(self, field: jnp.ndarray) -> jnp.ndarray:
|
|
903
|
+
values = _as_float64_array(field, "CutWallValueReconstructor3D.field")
|
|
904
|
+
gathered = values[self.neighbor_i, self.neighbor_j, self.neighbor_k]
|
|
905
|
+
return jnp.sum(self.weights * gathered, axis=1)
|
|
906
|
+
|
|
907
|
+
def extrapolate_neumann(
|
|
908
|
+
self,
|
|
909
|
+
field: jnp.ndarray,
|
|
910
|
+
normal_derivative: jnp.ndarray | float = 0.0,
|
|
911
|
+
) -> jnp.ndarray:
|
|
912
|
+
if self.bc_coeff is None:
|
|
913
|
+
raise ValueError("CutWallValueReconstructor3D.extrapolate_neumann requires bc_coeff")
|
|
914
|
+
base_value = self.extrapolate(field)
|
|
915
|
+
bc_value = _as_wall_face_array(
|
|
916
|
+
normal_derivative,
|
|
917
|
+
self.cut_wall_geometry.n_wall_faces,
|
|
918
|
+
"CutWallValueReconstructor3D.normal_derivative",
|
|
919
|
+
)
|
|
920
|
+
return base_value + self.bc_coeff * bc_value
|
|
921
|
+
|
|
922
|
+
def tree_flatten(self):
|
|
923
|
+
children = [self.cut_wall_geometry, self.neighbor_i, self.neighbor_j, self.neighbor_k, self.weights]
|
|
924
|
+
aux = self.bc_coeff is None
|
|
925
|
+
if self.bc_coeff is not None:
|
|
926
|
+
children.append(self.bc_coeff)
|
|
927
|
+
return tuple(children), aux
|
|
928
|
+
|
|
929
|
+
@classmethod
|
|
930
|
+
def tree_unflatten(cls, aux_data, children):
|
|
931
|
+
bc_coeff_is_none = aux_data
|
|
932
|
+
if bc_coeff_is_none:
|
|
933
|
+
cut_wall_geometry, neighbor_i, neighbor_j, neighbor_k, weights = children
|
|
934
|
+
return cls(cut_wall_geometry, neighbor_i, neighbor_j, neighbor_k, weights, None)
|
|
935
|
+
cut_wall_geometry, neighbor_i, neighbor_j, neighbor_k, weights, bc_coeff = children
|
|
936
|
+
return cls(cut_wall_geometry, neighbor_i, neighbor_j, neighbor_k, weights, bc_coeff)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@_pytree_base
|
|
940
|
+
@dataclass(frozen=True)
|
|
941
|
+
class CutWallNormalDerivativeConstructor3D:
|
|
942
|
+
"""Batched cut-wall normal derivative constructor for cell-centered fields.
|
|
943
|
+
|
|
944
|
+
Sign convention:
|
|
945
|
+
dnormal_from_wall_value(...) returns the outward normal derivative
|
|
946
|
+
d/dn, not the inward coordinate derivative d/ds. Any cut-wall
|
|
947
|
+
weight construction should therefore encode the outward normal sign
|
|
948
|
+
convention in the weights provided here.
|
|
949
|
+
"""
|
|
950
|
+
|
|
951
|
+
cut_wall_geometry: "CutWallGeometry3D"
|
|
952
|
+
neighbor_i: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
953
|
+
neighbor_j: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
954
|
+
neighbor_k: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
955
|
+
weights_dnormal: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
956
|
+
weights_d2normal: jnp.ndarray # (n_wall_faces, n_stencil)
|
|
957
|
+
wall_coeff_dnormal: jnp.ndarray # (n_wall_faces,)
|
|
958
|
+
wall_coeff_d2normal: jnp.ndarray # (n_wall_faces,)
|
|
959
|
+
|
|
960
|
+
def __post_init__(self) -> None:
|
|
961
|
+
if not isinstance(self.cut_wall_geometry, CutWallGeometry3D):
|
|
962
|
+
raise TypeError("CutWallNormalDerivativeConstructor3D.cut_wall_geometry must be a CutWallGeometry3D")
|
|
963
|
+
neighbor_i = _as_int_stencil_array(self.neighbor_i, "CutWallNormalDerivativeConstructor3D.neighbor_i")
|
|
964
|
+
neighbor_j = _as_int_stencil_array(self.neighbor_j, "CutWallNormalDerivativeConstructor3D.neighbor_j")
|
|
965
|
+
neighbor_k = _as_int_stencil_array(self.neighbor_k, "CutWallNormalDerivativeConstructor3D.neighbor_k")
|
|
966
|
+
weights_dnormal = _as_weight_stencil_array(
|
|
967
|
+
self.weights_dnormal,
|
|
968
|
+
"CutWallNormalDerivativeConstructor3D.weights_dnormal",
|
|
969
|
+
)
|
|
970
|
+
weights_d2normal = _as_weight_stencil_array(
|
|
971
|
+
self.weights_d2normal,
|
|
972
|
+
"CutWallNormalDerivativeConstructor3D.weights_d2normal",
|
|
973
|
+
)
|
|
974
|
+
expected_shape = neighbor_i.shape
|
|
975
|
+
for name, value in (
|
|
976
|
+
("neighbor_j", neighbor_j),
|
|
977
|
+
("neighbor_k", neighbor_k),
|
|
978
|
+
("weights_dnormal", weights_dnormal),
|
|
979
|
+
("weights_d2normal", weights_d2normal),
|
|
980
|
+
):
|
|
981
|
+
if value.shape != expected_shape:
|
|
982
|
+
raise ValueError(
|
|
983
|
+
"cut-wall normal derivative neighbor and weight arrays must all have "
|
|
984
|
+
f"shape {expected_shape}; {name} has shape {value.shape}"
|
|
985
|
+
)
|
|
986
|
+
if expected_shape[0] != self.cut_wall_geometry.n_wall_faces:
|
|
987
|
+
raise ValueError(
|
|
988
|
+
"cut-wall normal derivative arrays must have one row per cut-wall face; "
|
|
989
|
+
f"got {expected_shape[0]} rows and {self.cut_wall_geometry.n_wall_faces} faces"
|
|
990
|
+
)
|
|
991
|
+
wall_coeff_dnormal = _as_wall_face_array(
|
|
992
|
+
self.wall_coeff_dnormal,
|
|
993
|
+
self.cut_wall_geometry.n_wall_faces,
|
|
994
|
+
"CutWallNormalDerivativeConstructor3D.wall_coeff_dnormal",
|
|
995
|
+
)
|
|
996
|
+
wall_coeff_d2normal = _as_wall_face_array(
|
|
997
|
+
self.wall_coeff_d2normal,
|
|
998
|
+
self.cut_wall_geometry.n_wall_faces,
|
|
999
|
+
"CutWallNormalDerivativeConstructor3D.wall_coeff_d2normal",
|
|
1000
|
+
)
|
|
1001
|
+
object.__setattr__(self, "neighbor_i", neighbor_i)
|
|
1002
|
+
object.__setattr__(self, "neighbor_j", neighbor_j)
|
|
1003
|
+
object.__setattr__(self, "neighbor_k", neighbor_k)
|
|
1004
|
+
object.__setattr__(self, "weights_dnormal", weights_dnormal)
|
|
1005
|
+
object.__setattr__(self, "weights_d2normal", weights_d2normal)
|
|
1006
|
+
object.__setattr__(self, "wall_coeff_dnormal", wall_coeff_dnormal)
|
|
1007
|
+
object.__setattr__(self, "wall_coeff_d2normal", wall_coeff_d2normal)
|
|
1008
|
+
|
|
1009
|
+
@property
|
|
1010
|
+
def n_wall_faces(self) -> int:
|
|
1011
|
+
return self.cut_wall_geometry.n_wall_faces
|
|
1012
|
+
|
|
1013
|
+
def dnormal_from_wall_value(
|
|
1014
|
+
self,
|
|
1015
|
+
field: jnp.ndarray,
|
|
1016
|
+
wall_value: jnp.ndarray,
|
|
1017
|
+
) -> jnp.ndarray:
|
|
1018
|
+
values = _as_float64_array(field, "CutWallNormalDerivativeConstructor3D.field")
|
|
1019
|
+
wall = _as_wall_face_array(
|
|
1020
|
+
wall_value,
|
|
1021
|
+
self.cut_wall_geometry.n_wall_faces,
|
|
1022
|
+
"CutWallNormalDerivativeConstructor3D.wall_value",
|
|
1023
|
+
)
|
|
1024
|
+
gathered = values[self.neighbor_i, self.neighbor_j, self.neighbor_k]
|
|
1025
|
+
return jnp.sum(self.weights_dnormal * gathered, axis=1) + self.wall_coeff_dnormal * wall
|
|
1026
|
+
|
|
1027
|
+
def d2normal_from_wall_value(
|
|
1028
|
+
self,
|
|
1029
|
+
field: jnp.ndarray,
|
|
1030
|
+
wall_value: jnp.ndarray,
|
|
1031
|
+
) -> jnp.ndarray:
|
|
1032
|
+
values = _as_float64_array(field, "CutWallNormalDerivativeConstructor3D.field")
|
|
1033
|
+
wall = _as_wall_face_array(
|
|
1034
|
+
wall_value,
|
|
1035
|
+
self.cut_wall_geometry.n_wall_faces,
|
|
1036
|
+
"CutWallNormalDerivativeConstructor3D.wall_value",
|
|
1037
|
+
)
|
|
1038
|
+
gathered = values[self.neighbor_i, self.neighbor_j, self.neighbor_k]
|
|
1039
|
+
return jnp.sum(self.weights_d2normal * gathered, axis=1) + self.wall_coeff_d2normal * wall
|
|
1040
|
+
|
|
1041
|
+
def normal_derivatives_from_wall_value(
|
|
1042
|
+
self,
|
|
1043
|
+
field: jnp.ndarray,
|
|
1044
|
+
wall_value: jnp.ndarray,
|
|
1045
|
+
) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
1046
|
+
return (
|
|
1047
|
+
self.dnormal_from_wall_value(field, wall_value),
|
|
1048
|
+
self.d2normal_from_wall_value(field, wall_value),
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
def tree_flatten(self):
|
|
1052
|
+
return (
|
|
1053
|
+
(
|
|
1054
|
+
self.cut_wall_geometry,
|
|
1055
|
+
self.neighbor_i,
|
|
1056
|
+
self.neighbor_j,
|
|
1057
|
+
self.neighbor_k,
|
|
1058
|
+
self.weights_dnormal,
|
|
1059
|
+
self.weights_d2normal,
|
|
1060
|
+
self.wall_coeff_dnormal,
|
|
1061
|
+
self.wall_coeff_d2normal,
|
|
1062
|
+
),
|
|
1063
|
+
None,
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
@classmethod
|
|
1067
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1068
|
+
return cls(*children)
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
@_pytree_base
|
|
1072
|
+
@dataclass(frozen=True)
|
|
1073
|
+
class LocalStencil1D:
|
|
1074
|
+
"""Field-dependent 1D stencil values for one coordinate direction."""
|
|
1075
|
+
|
|
1076
|
+
center: jnp.ndarray
|
|
1077
|
+
minus: jnp.ndarray
|
|
1078
|
+
plus: jnp.ndarray
|
|
1079
|
+
dx_min: jnp.ndarray
|
|
1080
|
+
dx_plus: jnp.ndarray
|
|
1081
|
+
derivative_minus_weight: jnp.ndarray | None = None
|
|
1082
|
+
derivative_center_weight: jnp.ndarray | None = None
|
|
1083
|
+
derivative_plus_weight: jnp.ndarray | None = None
|
|
1084
|
+
|
|
1085
|
+
def __post_init__(self) -> None:
|
|
1086
|
+
center = _as_float64_array(self.center, "center")
|
|
1087
|
+
shape = center.shape
|
|
1088
|
+
|
|
1089
|
+
for name in ("minus", "plus", "dx_min", "dx_plus"):
|
|
1090
|
+
value = _as_float64_array(getattr(self, name), f"LocalStencil1D.{name}")
|
|
1091
|
+
if value.shape != shape:
|
|
1092
|
+
raise ValueError(
|
|
1093
|
+
f"LocalStencil1D.{name} must have shape {shape}, got {value.shape}"
|
|
1094
|
+
)
|
|
1095
|
+
object.__setattr__(self, name, value)
|
|
1096
|
+
|
|
1097
|
+
derivative_weight_names = (
|
|
1098
|
+
"derivative_minus_weight",
|
|
1099
|
+
"derivative_center_weight",
|
|
1100
|
+
"derivative_plus_weight",
|
|
1101
|
+
)
|
|
1102
|
+
supplied_weights = [getattr(self, name) is not None for name in derivative_weight_names]
|
|
1103
|
+
if any(supplied_weights) and not all(supplied_weights):
|
|
1104
|
+
raise ValueError(
|
|
1105
|
+
"LocalStencil1D derivative weights must either all be supplied or all be omitted"
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
if all(supplied_weights):
|
|
1109
|
+
for name in derivative_weight_names:
|
|
1110
|
+
value = _as_float64_array(getattr(self, name), f"LocalStencil1D.{name}")
|
|
1111
|
+
if value.shape != shape:
|
|
1112
|
+
raise ValueError(
|
|
1113
|
+
f"LocalStencil1D.{name} must have shape {shape}, got {value.shape}"
|
|
1114
|
+
)
|
|
1115
|
+
object.__setattr__(self, name, value)
|
|
1116
|
+
else:
|
|
1117
|
+
dx_min = jnp.asarray(self.dx_min, dtype=jnp.float64)
|
|
1118
|
+
dx_plus = jnp.asarray(self.dx_plus, dtype=jnp.float64)
|
|
1119
|
+
denom = jnp.maximum(dx_min * dx_plus * (dx_min + dx_plus), 1.0e-30)
|
|
1120
|
+
object.__setattr__(self, "derivative_minus_weight", -dx_plus * dx_plus / denom)
|
|
1121
|
+
object.__setattr__(
|
|
1122
|
+
self,
|
|
1123
|
+
"derivative_center_weight",
|
|
1124
|
+
(dx_plus * dx_plus - dx_min * dx_min) / denom,
|
|
1125
|
+
)
|
|
1126
|
+
object.__setattr__(self, "derivative_plus_weight", dx_min * dx_min / denom)
|
|
1127
|
+
|
|
1128
|
+
object.__setattr__(self, "center", center)
|
|
1129
|
+
|
|
1130
|
+
@property
|
|
1131
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1132
|
+
return tuple(int(v) for v in self.center.shape)
|
|
1133
|
+
|
|
1134
|
+
def replace(self, **updates: object) -> "LocalStencil1D":
|
|
1135
|
+
"""Return a new stencil with one or more fields replaced."""
|
|
1136
|
+
|
|
1137
|
+
allowed = {
|
|
1138
|
+
"center",
|
|
1139
|
+
"minus",
|
|
1140
|
+
"plus",
|
|
1141
|
+
"dx_min",
|
|
1142
|
+
"dx_plus",
|
|
1143
|
+
"derivative_minus_weight",
|
|
1144
|
+
"derivative_center_weight",
|
|
1145
|
+
"derivative_plus_weight",
|
|
1146
|
+
}
|
|
1147
|
+
unknown = set(updates) - allowed
|
|
1148
|
+
if unknown:
|
|
1149
|
+
names = ", ".join(sorted(unknown))
|
|
1150
|
+
raise ValueError(f"Unknown LocalStencil1D field(s): {names}")
|
|
1151
|
+
return dataclass_replace_1d(self, **updates)
|
|
1152
|
+
|
|
1153
|
+
def tree_flatten(self):
|
|
1154
|
+
return (
|
|
1155
|
+
(
|
|
1156
|
+
self.center,
|
|
1157
|
+
self.minus,
|
|
1158
|
+
self.plus,
|
|
1159
|
+
self.dx_min,
|
|
1160
|
+
self.dx_plus,
|
|
1161
|
+
self.derivative_minus_weight,
|
|
1162
|
+
self.derivative_center_weight,
|
|
1163
|
+
self.derivative_plus_weight,
|
|
1164
|
+
),
|
|
1165
|
+
None,
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
@classmethod
|
|
1169
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1170
|
+
return cls(*children)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
@_pytree_base
|
|
1174
|
+
@dataclass(frozen=True)
|
|
1175
|
+
class LocalStencil3D:
|
|
1176
|
+
"""Nested 3D stencil for local/pointwise operators.
|
|
1177
|
+
|
|
1178
|
+
This stencil is the reconstruction object for local derivatives and
|
|
1179
|
+
pointwise operators. It may be boundary-aware, including embedded-wall
|
|
1180
|
+
aware reconstruction logic in the caller that builds it.
|
|
1181
|
+
"""
|
|
1182
|
+
|
|
1183
|
+
x: LocalStencil1D
|
|
1184
|
+
y: LocalStencil1D
|
|
1185
|
+
z: LocalStencil1D
|
|
1186
|
+
|
|
1187
|
+
def __post_init__(self) -> None:
|
|
1188
|
+
if not isinstance(self.x, LocalStencil1D):
|
|
1189
|
+
raise TypeError("LocalStencil3D.x must be a LocalStencil1D")
|
|
1190
|
+
if not isinstance(self.y, LocalStencil1D):
|
|
1191
|
+
raise TypeError("LocalStencil3D.y must be a LocalStencil1D")
|
|
1192
|
+
if not isinstance(self.z, LocalStencil1D):
|
|
1193
|
+
raise TypeError("LocalStencil3D.z must be a LocalStencil1D")
|
|
1194
|
+
if self.x.shape != self.y.shape or self.x.shape != self.z.shape:
|
|
1195
|
+
raise ValueError(
|
|
1196
|
+
"LocalStencil3D axis stencils must all have the same shape; "
|
|
1197
|
+
f"got x={self.x.shape}, y={self.y.shape}, z={self.z.shape}"
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
@property
|
|
1201
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1202
|
+
return self.x.shape
|
|
1203
|
+
|
|
1204
|
+
def replace(self, **updates: object) -> "LocalStencil3D":
|
|
1205
|
+
"""Return a new stencil with one or more fields replaced."""
|
|
1206
|
+
|
|
1207
|
+
allowed = {"x", "y", "z"}
|
|
1208
|
+
unknown = set(updates) - allowed
|
|
1209
|
+
if unknown:
|
|
1210
|
+
names = ", ".join(sorted(unknown))
|
|
1211
|
+
raise ValueError(f"Unknown LocalStencil3D field(s): {names}")
|
|
1212
|
+
return dataclass_replace_3d(self, **updates)
|
|
1213
|
+
|
|
1214
|
+
def tree_flatten(self):
|
|
1215
|
+
return ((self.x, self.y, self.z), None)
|
|
1216
|
+
|
|
1217
|
+
@classmethod
|
|
1218
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1219
|
+
return cls(*children)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
@_pytree_base
|
|
1223
|
+
@dataclass(frozen=True)
|
|
1224
|
+
class FaceGradientStencil3D:
|
|
1225
|
+
"""Face-centered coordinate gradients for a scalar field."""
|
|
1226
|
+
|
|
1227
|
+
x: jnp.ndarray
|
|
1228
|
+
y: jnp.ndarray
|
|
1229
|
+
z: jnp.ndarray
|
|
1230
|
+
|
|
1231
|
+
def __post_init__(self) -> None:
|
|
1232
|
+
x = jnp.asarray(self.x, dtype=jnp.float64)
|
|
1233
|
+
y = jnp.asarray(self.y, dtype=jnp.float64)
|
|
1234
|
+
z = jnp.asarray(self.z, dtype=jnp.float64)
|
|
1235
|
+
|
|
1236
|
+
for name, value in (("x", x), ("y", y), ("z", z)):
|
|
1237
|
+
if value.ndim != 4 or value.shape[-1] != 3:
|
|
1238
|
+
raise ValueError(
|
|
1239
|
+
f"FaceGradientStencil3D.{name} must have shape (nx, ny, nz, 3), got {value.shape}"
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
object.__setattr__(self, "x", x)
|
|
1243
|
+
object.__setattr__(self, "y", y)
|
|
1244
|
+
object.__setattr__(self, "z", z)
|
|
1245
|
+
|
|
1246
|
+
@property
|
|
1247
|
+
def shape(self) -> tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]:
|
|
1248
|
+
return tuple(int(v) for v in self.x.shape[:-1]), tuple(int(v) for v in self.y.shape[:-1]), tuple(int(v) for v in self.z.shape[:-1])
|
|
1249
|
+
|
|
1250
|
+
def tree_flatten(self):
|
|
1251
|
+
return ((self.x, self.y, self.z), None)
|
|
1252
|
+
|
|
1253
|
+
@classmethod
|
|
1254
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1255
|
+
return cls(*children)
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
@_pytree_base
|
|
1259
|
+
@dataclass(frozen=True)
|
|
1260
|
+
class ConservativeStencil3D:
|
|
1261
|
+
"""Nested 3D stencil for conservative operators.
|
|
1262
|
+
|
|
1263
|
+
This is intentionally separate from ``LocalStencil3D`` so conservative
|
|
1264
|
+
flux assembly cannot accidentally inherit embedded-wall reconstruction
|
|
1265
|
+
semantics. The payload is still cell-centered reconstruction data, but the
|
|
1266
|
+
meaning is restricted to regular control-volume flux construction.
|
|
1267
|
+
"""
|
|
1268
|
+
|
|
1269
|
+
x: LocalStencil1D
|
|
1270
|
+
y: LocalStencil1D
|
|
1271
|
+
z: LocalStencil1D
|
|
1272
|
+
face_grad: FaceGradientStencil3D
|
|
1273
|
+
|
|
1274
|
+
def __post_init__(self) -> None:
|
|
1275
|
+
if not isinstance(self.x, LocalStencil1D):
|
|
1276
|
+
raise TypeError("ConservativeStencil3D.x must be a LocalStencil1D")
|
|
1277
|
+
if not isinstance(self.y, LocalStencil1D):
|
|
1278
|
+
raise TypeError("ConservativeStencil3D.y must be a LocalStencil1D")
|
|
1279
|
+
if not isinstance(self.z, LocalStencil1D):
|
|
1280
|
+
raise TypeError("ConservativeStencil3D.z must be a LocalStencil1D")
|
|
1281
|
+
if not isinstance(self.face_grad, FaceGradientStencil3D):
|
|
1282
|
+
raise TypeError("ConservativeStencil3D.face_grad must be a FaceGradientStencil3D")
|
|
1283
|
+
if self.x.shape != self.y.shape or self.x.shape != self.z.shape:
|
|
1284
|
+
raise ValueError(
|
|
1285
|
+
"ConservativeStencil3D axis stencils must all have the same shape; "
|
|
1286
|
+
f"got x={self.x.shape}, y={self.y.shape}, z={self.z.shape}"
|
|
1287
|
+
)
|
|
1288
|
+
expected_x = (self.x.shape[0] + 1, self.x.shape[1], self.x.shape[2])
|
|
1289
|
+
expected_y = (self.x.shape[0], self.x.shape[1] + 1, self.x.shape[2])
|
|
1290
|
+
expected_z = (self.x.shape[0], self.x.shape[1], self.x.shape[2] + 1)
|
|
1291
|
+
if self.face_grad.x.shape[:-1] != expected_x:
|
|
1292
|
+
raise ValueError(
|
|
1293
|
+
f"ConservativeStencil3D.face_grad.x must have shape {expected_x + (3,)}, got {self.face_grad.x.shape}"
|
|
1294
|
+
)
|
|
1295
|
+
if self.face_grad.y.shape[:-1] != expected_y:
|
|
1296
|
+
raise ValueError(
|
|
1297
|
+
f"ConservativeStencil3D.face_grad.y must have shape {expected_y + (3,)}, got {self.face_grad.y.shape}"
|
|
1298
|
+
)
|
|
1299
|
+
if self.face_grad.z.shape[:-1] != expected_z:
|
|
1300
|
+
raise ValueError(
|
|
1301
|
+
f"ConservativeStencil3D.face_grad.z must have shape {expected_z + (3,)}, got {self.face_grad.z.shape}"
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
@property
|
|
1305
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1306
|
+
return self.x.shape
|
|
1307
|
+
|
|
1308
|
+
def replace(self, **updates: object) -> "ConservativeStencil3D":
|
|
1309
|
+
"""Return a new stencil with one or more fields replaced."""
|
|
1310
|
+
|
|
1311
|
+
allowed = {"x", "y", "z", "face_grad"}
|
|
1312
|
+
unknown = set(updates) - allowed
|
|
1313
|
+
if unknown:
|
|
1314
|
+
names = ", ".join(sorted(unknown))
|
|
1315
|
+
raise ValueError(f"Unknown ConservativeStencil3D field(s): {names}")
|
|
1316
|
+
return dataclass_replace_conservative(self, **updates)
|
|
1317
|
+
|
|
1318
|
+
def tree_flatten(self):
|
|
1319
|
+
return ((self.x, self.y, self.z, self.face_grad), None)
|
|
1320
|
+
|
|
1321
|
+
@classmethod
|
|
1322
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1323
|
+
return cls(*children)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
@_pytree_base
|
|
1327
|
+
@dataclass(frozen=True)
|
|
1328
|
+
class FaceFluxStencil3D:
|
|
1329
|
+
"""Face-centered flux arrays for the three coordinate directions.
|
|
1330
|
+
|
|
1331
|
+
In the local domain-decomposed path, the inferred shape is the local
|
|
1332
|
+
owned-cell shape. These are the control faces needed to update owned
|
|
1333
|
+
cells, not necessarily uniquely owned global faces.
|
|
1334
|
+
"""
|
|
1335
|
+
|
|
1336
|
+
x: jnp.ndarray
|
|
1337
|
+
y: jnp.ndarray
|
|
1338
|
+
z: jnp.ndarray
|
|
1339
|
+
|
|
1340
|
+
def __post_init__(self) -> None:
|
|
1341
|
+
x = _as_face_flux_array(self.x, "FaceFluxStencil3D.x")
|
|
1342
|
+
y = _as_face_flux_array(self.y, "FaceFluxStencil3D.y")
|
|
1343
|
+
z = _as_face_flux_array(self.z, "FaceFluxStencil3D.z")
|
|
1344
|
+
|
|
1345
|
+
cell_shape = (x.shape[0] - 1, y.shape[1] - 1, z.shape[2] - 1)
|
|
1346
|
+
expected_x = (cell_shape[0] + 1, cell_shape[1], cell_shape[2])
|
|
1347
|
+
expected_y = (cell_shape[0], cell_shape[1] + 1, cell_shape[2])
|
|
1348
|
+
expected_z = (cell_shape[0], cell_shape[1], cell_shape[2] + 1)
|
|
1349
|
+
if x.shape != expected_x or y.shape != expected_y or z.shape != expected_z:
|
|
1350
|
+
raise ValueError(
|
|
1351
|
+
"FaceFluxStencil3D axis shapes must match the face-grid layout; "
|
|
1352
|
+
f"expected x={expected_x}, y={expected_y}, z={expected_z}, got "
|
|
1353
|
+
f"x={x.shape}, y={y.shape}, z={z.shape}"
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
object.__setattr__(self, "x", x)
|
|
1357
|
+
object.__setattr__(self, "y", y)
|
|
1358
|
+
object.__setattr__(self, "z", z)
|
|
1359
|
+
|
|
1360
|
+
@property
|
|
1361
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1362
|
+
return (int(self.x.shape[0] - 1), int(self.y.shape[1] - 1), int(self.z.shape[2] - 1))
|
|
1363
|
+
|
|
1364
|
+
def tree_flatten(self):
|
|
1365
|
+
return ((self.x, self.y, self.z), None)
|
|
1366
|
+
|
|
1367
|
+
@classmethod
|
|
1368
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1369
|
+
return cls(*children)
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
@_pytree_base
|
|
1373
|
+
@dataclass(frozen=True)
|
|
1374
|
+
class BoundaryFaceBC3D:
|
|
1375
|
+
"""Dense face-grid boundary-condition data for regular coordinate faces."""
|
|
1376
|
+
|
|
1377
|
+
kind_x: jnp.ndarray # (nx + 1, ny, nz)
|
|
1378
|
+
kind_y: jnp.ndarray # (nx, ny + 1, nz)
|
|
1379
|
+
kind_z: jnp.ndarray # (nx, ny, nz + 1)
|
|
1380
|
+
value_x: jnp.ndarray # (nx + 1, ny, nz)
|
|
1381
|
+
value_y: jnp.ndarray # (nx, ny + 1, nz)
|
|
1382
|
+
value_z: jnp.ndarray # (nx, ny, nz + 1)
|
|
1383
|
+
mask_x: jnp.ndarray # (nx + 1, ny, nz)
|
|
1384
|
+
mask_y: jnp.ndarray # (nx, ny + 1, nz)
|
|
1385
|
+
mask_z: jnp.ndarray # (nx, ny, nz + 1)
|
|
1386
|
+
|
|
1387
|
+
def __post_init__(self) -> None:
|
|
1388
|
+
kind_x = _as_int_face_array(self.kind_x, "BoundaryFaceBC3D.kind_x")
|
|
1389
|
+
kind_y = _as_int_face_array(self.kind_y, "BoundaryFaceBC3D.kind_y")
|
|
1390
|
+
kind_z = _as_int_face_array(self.kind_z, "BoundaryFaceBC3D.kind_z")
|
|
1391
|
+
value_x = _as_face_flux_array(self.value_x, "BoundaryFaceBC3D.value_x")
|
|
1392
|
+
value_y = _as_face_flux_array(self.value_y, "BoundaryFaceBC3D.value_y")
|
|
1393
|
+
value_z = _as_face_flux_array(self.value_z, "BoundaryFaceBC3D.value_z")
|
|
1394
|
+
mask_x = _as_bool_face_array(self.mask_x, "BoundaryFaceBC3D.mask_x")
|
|
1395
|
+
mask_y = _as_bool_face_array(self.mask_y, "BoundaryFaceBC3D.mask_y")
|
|
1396
|
+
mask_z = _as_bool_face_array(self.mask_z, "BoundaryFaceBC3D.mask_z")
|
|
1397
|
+
if kind_x.shape != value_x.shape or kind_x.shape != mask_x.shape:
|
|
1398
|
+
raise ValueError("BoundaryFaceBC3D.x arrays must all have the same shape")
|
|
1399
|
+
if kind_y.shape != value_y.shape or kind_y.shape != mask_y.shape:
|
|
1400
|
+
raise ValueError("BoundaryFaceBC3D.y arrays must all have the same shape")
|
|
1401
|
+
if kind_z.shape != value_z.shape or kind_z.shape != mask_z.shape:
|
|
1402
|
+
raise ValueError("BoundaryFaceBC3D.z arrays must all have the same shape")
|
|
1403
|
+
object.__setattr__(self, "kind_x", kind_x)
|
|
1404
|
+
object.__setattr__(self, "kind_y", kind_y)
|
|
1405
|
+
object.__setattr__(self, "kind_z", kind_z)
|
|
1406
|
+
object.__setattr__(self, "value_x", value_x)
|
|
1407
|
+
object.__setattr__(self, "value_y", value_y)
|
|
1408
|
+
object.__setattr__(self, "value_z", value_z)
|
|
1409
|
+
object.__setattr__(self, "mask_x", mask_x)
|
|
1410
|
+
object.__setattr__(self, "mask_y", mask_y)
|
|
1411
|
+
object.__setattr__(self, "mask_z", mask_z)
|
|
1412
|
+
|
|
1413
|
+
@classmethod
|
|
1414
|
+
def empty(cls, geometry: RegularFaceGeometry3D) -> "BoundaryFaceBC3D":
|
|
1415
|
+
return cls(
|
|
1416
|
+
kind_x=jnp.zeros_like(geometry.x_area, dtype=jnp.int32),
|
|
1417
|
+
kind_y=jnp.zeros_like(geometry.y_area, dtype=jnp.int32),
|
|
1418
|
+
kind_z=jnp.zeros_like(geometry.z_area, dtype=jnp.int32),
|
|
1419
|
+
value_x=jnp.zeros_like(geometry.x_area, dtype=jnp.float64),
|
|
1420
|
+
value_y=jnp.zeros_like(geometry.y_area, dtype=jnp.float64),
|
|
1421
|
+
value_z=jnp.zeros_like(geometry.z_area, dtype=jnp.float64),
|
|
1422
|
+
mask_x=jnp.zeros_like(geometry.x_open_mask, dtype=bool),
|
|
1423
|
+
mask_y=jnp.zeros_like(geometry.y_open_mask, dtype=bool),
|
|
1424
|
+
mask_z=jnp.zeros_like(geometry.z_open_mask, dtype=bool),
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
def tree_flatten(self):
|
|
1428
|
+
return (
|
|
1429
|
+
(
|
|
1430
|
+
self.kind_x,
|
|
1431
|
+
self.kind_y,
|
|
1432
|
+
self.kind_z,
|
|
1433
|
+
self.value_x,
|
|
1434
|
+
self.value_y,
|
|
1435
|
+
self.value_z,
|
|
1436
|
+
self.mask_x,
|
|
1437
|
+
self.mask_y,
|
|
1438
|
+
self.mask_z,
|
|
1439
|
+
),
|
|
1440
|
+
None,
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
@classmethod
|
|
1444
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1445
|
+
return cls(*children)
|
|
1446
|
+
|
|
1447
|
+
def replace(self, **updates: object) -> "BoundaryFaceBC3D":
|
|
1448
|
+
allowed = {
|
|
1449
|
+
"kind_x",
|
|
1450
|
+
"kind_y",
|
|
1451
|
+
"kind_z",
|
|
1452
|
+
"value_x",
|
|
1453
|
+
"value_y",
|
|
1454
|
+
"value_z",
|
|
1455
|
+
"mask_x",
|
|
1456
|
+
"mask_y",
|
|
1457
|
+
"mask_z",
|
|
1458
|
+
}
|
|
1459
|
+
unknown = set(updates) - allowed
|
|
1460
|
+
if unknown:
|
|
1461
|
+
names = ", ".join(sorted(unknown))
|
|
1462
|
+
raise ValueError(f"Unknown BoundaryFaceBC3D field(s): {names}")
|
|
1463
|
+
return BoundaryFaceBC3D(
|
|
1464
|
+
kind_x=updates.get("kind_x", self.kind_x),
|
|
1465
|
+
kind_y=updates.get("kind_y", self.kind_y),
|
|
1466
|
+
kind_z=updates.get("kind_z", self.kind_z),
|
|
1467
|
+
value_x=updates.get("value_x", self.value_x),
|
|
1468
|
+
value_y=updates.get("value_y", self.value_y),
|
|
1469
|
+
value_z=updates.get("value_z", self.value_z),
|
|
1470
|
+
mask_x=updates.get("mask_x", self.mask_x),
|
|
1471
|
+
mask_y=updates.get("mask_y", self.mask_y),
|
|
1472
|
+
mask_z=updates.get("mask_z", self.mask_z),
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
@_pytree_base
|
|
1477
|
+
@dataclass(frozen=True)
|
|
1478
|
+
class LocalBoundaryFaceBC3D(_DataclassPyTreeMixin):
|
|
1479
|
+
"""
|
|
1480
|
+
Local physical regular-coordinate face boundary-condition payload.
|
|
1481
|
+
|
|
1482
|
+
Arrays are dense over local owned control faces, but masks are true only on
|
|
1483
|
+
true physical coordinate boundary faces touched by this local shard.
|
|
1484
|
+
|
|
1485
|
+
Internal shard interfaces, periodic interfaces, axis/topological fills, and
|
|
1486
|
+
ordinary interior faces must have mask=False.
|
|
1487
|
+
|
|
1488
|
+
This object is the single source of truth for physical coordinate-face BCs.
|
|
1489
|
+
It is consumed by conservative flux builders and, optionally, by ghost-cell
|
|
1490
|
+
fillers for operators that choose ghost-materialized BC enforcement.
|
|
1491
|
+
"""
|
|
1492
|
+
|
|
1493
|
+
kind_x: jnp.ndarray
|
|
1494
|
+
kind_y: jnp.ndarray
|
|
1495
|
+
kind_z: jnp.ndarray
|
|
1496
|
+
value_x: jnp.ndarray
|
|
1497
|
+
value_y: jnp.ndarray
|
|
1498
|
+
value_z: jnp.ndarray
|
|
1499
|
+
mask_x: jnp.ndarray
|
|
1500
|
+
mask_y: jnp.ndarray
|
|
1501
|
+
mask_z: jnp.ndarray
|
|
1502
|
+
layout: HaloLayout3D
|
|
1503
|
+
|
|
1504
|
+
def __post_init__(self) -> None:
|
|
1505
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
1506
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
1507
|
+
|
|
1508
|
+
expected_x = self.layout.face_control_shape(axis=0)
|
|
1509
|
+
expected_y = self.layout.face_control_shape(axis=1)
|
|
1510
|
+
expected_z = self.layout.face_control_shape(axis=2)
|
|
1511
|
+
|
|
1512
|
+
kind_x = jnp.asarray(self.kind_x, dtype=jnp.int32)
|
|
1513
|
+
kind_y = jnp.asarray(self.kind_y, dtype=jnp.int32)
|
|
1514
|
+
kind_z = jnp.asarray(self.kind_z, dtype=jnp.int32)
|
|
1515
|
+
value_x = jnp.asarray(self.value_x, dtype=jnp.float64)
|
|
1516
|
+
value_y = jnp.asarray(self.value_y, dtype=jnp.float64)
|
|
1517
|
+
value_z = jnp.asarray(self.value_z, dtype=jnp.float64)
|
|
1518
|
+
mask_x = jnp.asarray(self.mask_x, dtype=bool)
|
|
1519
|
+
mask_y = jnp.asarray(self.mask_y, dtype=bool)
|
|
1520
|
+
mask_z = jnp.asarray(self.mask_z, dtype=bool)
|
|
1521
|
+
|
|
1522
|
+
for name, value, expected in (
|
|
1523
|
+
("kind_x", kind_x, expected_x),
|
|
1524
|
+
("kind_y", kind_y, expected_y),
|
|
1525
|
+
("kind_z", kind_z, expected_z),
|
|
1526
|
+
("value_x", value_x, expected_x),
|
|
1527
|
+
("value_y", value_y, expected_y),
|
|
1528
|
+
("value_z", value_z, expected_z),
|
|
1529
|
+
("mask_x", mask_x, expected_x),
|
|
1530
|
+
("mask_y", mask_y, expected_y),
|
|
1531
|
+
("mask_z", mask_z, expected_z),
|
|
1532
|
+
):
|
|
1533
|
+
if value.shape != expected:
|
|
1534
|
+
raise ValueError(
|
|
1535
|
+
f"LocalBoundaryFaceBC3D.{name} must have shape {expected}, got {value.shape}"
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
kind_x = jnp.where(mask_x, kind_x, BC_NONE)
|
|
1539
|
+
kind_y = jnp.where(mask_y, kind_y, BC_NONE)
|
|
1540
|
+
kind_z = jnp.where(mask_z, kind_z, BC_NONE)
|
|
1541
|
+
value_x = jnp.where(mask_x, value_x, 0.0)
|
|
1542
|
+
value_y = jnp.where(mask_y, value_y, 0.0)
|
|
1543
|
+
value_z = jnp.where(mask_z, value_z, 0.0)
|
|
1544
|
+
|
|
1545
|
+
object.__setattr__(self, "kind_x", kind_x)
|
|
1546
|
+
object.__setattr__(self, "kind_y", kind_y)
|
|
1547
|
+
object.__setattr__(self, "kind_z", kind_z)
|
|
1548
|
+
object.__setattr__(self, "value_x", value_x)
|
|
1549
|
+
object.__setattr__(self, "value_y", value_y)
|
|
1550
|
+
object.__setattr__(self, "value_z", value_z)
|
|
1551
|
+
object.__setattr__(self, "mask_x", mask_x)
|
|
1552
|
+
object.__setattr__(self, "mask_y", mask_y)
|
|
1553
|
+
object.__setattr__(self, "mask_z", mask_z)
|
|
1554
|
+
|
|
1555
|
+
@property
|
|
1556
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1557
|
+
return self.layout.owned_shape
|
|
1558
|
+
|
|
1559
|
+
@classmethod
|
|
1560
|
+
def empty(cls, layout: HaloLayout3D) -> "LocalBoundaryFaceBC3D":
|
|
1561
|
+
x_shape = layout.face_control_shape(axis=0)
|
|
1562
|
+
y_shape = layout.face_control_shape(axis=1)
|
|
1563
|
+
z_shape = layout.face_control_shape(axis=2)
|
|
1564
|
+
return cls(
|
|
1565
|
+
kind_x=jnp.zeros(x_shape, dtype=jnp.int32),
|
|
1566
|
+
kind_y=jnp.zeros(y_shape, dtype=jnp.int32),
|
|
1567
|
+
kind_z=jnp.zeros(z_shape, dtype=jnp.int32),
|
|
1568
|
+
value_x=jnp.zeros(x_shape, dtype=jnp.float64),
|
|
1569
|
+
value_y=jnp.zeros(y_shape, dtype=jnp.float64),
|
|
1570
|
+
value_z=jnp.zeros(z_shape, dtype=jnp.float64),
|
|
1571
|
+
mask_x=jnp.zeros(x_shape, dtype=bool),
|
|
1572
|
+
mask_y=jnp.zeros(y_shape, dtype=bool),
|
|
1573
|
+
mask_z=jnp.zeros(z_shape, dtype=bool),
|
|
1574
|
+
layout=layout,
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
def tree_flatten(self):
|
|
1578
|
+
children = (
|
|
1579
|
+
self.kind_x,
|
|
1580
|
+
self.kind_y,
|
|
1581
|
+
self.kind_z,
|
|
1582
|
+
self.value_x,
|
|
1583
|
+
self.value_y,
|
|
1584
|
+
self.value_z,
|
|
1585
|
+
self.mask_x,
|
|
1586
|
+
self.mask_y,
|
|
1587
|
+
self.mask_z,
|
|
1588
|
+
)
|
|
1589
|
+
aux_data = self.layout
|
|
1590
|
+
return children, aux_data
|
|
1591
|
+
|
|
1592
|
+
@classmethod
|
|
1593
|
+
def tree_unflatten(cls, aux_data, children):
|
|
1594
|
+
layout = aux_data
|
|
1595
|
+
(
|
|
1596
|
+
kind_x,
|
|
1597
|
+
kind_y,
|
|
1598
|
+
kind_z,
|
|
1599
|
+
value_x,
|
|
1600
|
+
value_y,
|
|
1601
|
+
value_z,
|
|
1602
|
+
mask_x,
|
|
1603
|
+
mask_y,
|
|
1604
|
+
mask_z,
|
|
1605
|
+
) = children
|
|
1606
|
+
return cls(
|
|
1607
|
+
kind_x=kind_x,
|
|
1608
|
+
kind_y=kind_y,
|
|
1609
|
+
kind_z=kind_z,
|
|
1610
|
+
value_x=value_x,
|
|
1611
|
+
value_y=value_y,
|
|
1612
|
+
value_z=value_z,
|
|
1613
|
+
mask_x=mask_x,
|
|
1614
|
+
mask_y=mask_y,
|
|
1615
|
+
mask_z=mask_z,
|
|
1616
|
+
layout=layout,
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
@_pytree_base
|
|
1621
|
+
@dataclass(frozen=True)
|
|
1622
|
+
class BoundaryConditionBuilder(Generic[BoundaryPayloadT]):
|
|
1623
|
+
"""Callable adapter that delegates boundary-payload construction to an injected function."""
|
|
1624
|
+
|
|
1625
|
+
build_fn: Callable[
|
|
1626
|
+
[
|
|
1627
|
+
Any,
|
|
1628
|
+
"FciGeometry3D",
|
|
1629
|
+
tuple[bool | None, bool | None, bool | None] | None,
|
|
1630
|
+
"CutWallGeometry3D | None",
|
|
1631
|
+
"CutWallBC3D | None",
|
|
1632
|
+
],
|
|
1633
|
+
BoundaryPayloadT,
|
|
1634
|
+
]
|
|
1635
|
+
|
|
1636
|
+
def __call__(
|
|
1637
|
+
self,
|
|
1638
|
+
state: Any,
|
|
1639
|
+
geometry: "FciGeometry3D",
|
|
1640
|
+
periodic_axes: tuple[bool | None, bool | None, bool | None] | None,
|
|
1641
|
+
cut_wall_geometry: "CutWallGeometry3D | None",
|
|
1642
|
+
cut_wall_bc: "CutWallBC3D | None",
|
|
1643
|
+
) -> BoundaryPayloadT:
|
|
1644
|
+
return self.build_fn(state, geometry, periodic_axes, cut_wall_geometry, cut_wall_bc)
|
|
1645
|
+
|
|
1646
|
+
def tree_flatten(self):
|
|
1647
|
+
return (), self.build_fn
|
|
1648
|
+
|
|
1649
|
+
@classmethod
|
|
1650
|
+
def tree_unflatten(cls, aux_data, children):
|
|
1651
|
+
return cls(aux_data)
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
@_pytree_base
|
|
1655
|
+
@dataclass(frozen=True)
|
|
1656
|
+
class CutWallGeometry3D:
|
|
1657
|
+
"""Geometry and metric data for true non-coordinate cut-wall faces."""
|
|
1658
|
+
|
|
1659
|
+
owner_i: jnp.ndarray
|
|
1660
|
+
owner_j: jnp.ndarray
|
|
1661
|
+
owner_k: jnp.ndarray
|
|
1662
|
+
center: jnp.ndarray # (n_wall_faces, 3)
|
|
1663
|
+
normal_contra: jnp.ndarray # (n_wall_faces, 3), outward normal from the computational domain
|
|
1664
|
+
area_covector: jnp.ndarray # (n_wall_faces, 3), outward-oriented area covector
|
|
1665
|
+
distance: jnp.ndarray # (n_wall_faces,)
|
|
1666
|
+
J: jnp.ndarray # (n_wall_faces,)
|
|
1667
|
+
g_contra: jnp.ndarray # (n_wall_faces, 3, 3)
|
|
1668
|
+
g_cov: jnp.ndarray # (n_wall_faces, 3, 3)
|
|
1669
|
+
B_contra: jnp.ndarray # (n_wall_faces, 3)
|
|
1670
|
+
Bmag: jnp.ndarray # (n_wall_faces,)
|
|
1671
|
+
sign: jnp.ndarray # (n_wall_faces,), orientation sign relative to the owner cell / outward domain normal
|
|
1672
|
+
|
|
1673
|
+
def __post_init__(self) -> None:
|
|
1674
|
+
owner_i = jnp.asarray(self.owner_i, dtype=jnp.int32)
|
|
1675
|
+
owner_j = jnp.asarray(self.owner_j, dtype=jnp.int32)
|
|
1676
|
+
owner_k = jnp.asarray(self.owner_k, dtype=jnp.int32)
|
|
1677
|
+
center = jnp.asarray(self.center, dtype=jnp.float64)
|
|
1678
|
+
normal_contra = jnp.asarray(self.normal_contra, dtype=jnp.float64)
|
|
1679
|
+
area_covector = jnp.asarray(self.area_covector, dtype=jnp.float64)
|
|
1680
|
+
distance = jnp.asarray(self.distance, dtype=jnp.float64)
|
|
1681
|
+
J = jnp.asarray(self.J, dtype=jnp.float64)
|
|
1682
|
+
g_contra = jnp.asarray(self.g_contra, dtype=jnp.float64)
|
|
1683
|
+
g_cov = jnp.asarray(self.g_cov, dtype=jnp.float64)
|
|
1684
|
+
B_contra = jnp.asarray(self.B_contra, dtype=jnp.float64)
|
|
1685
|
+
Bmag = jnp.asarray(self.Bmag, dtype=jnp.float64)
|
|
1686
|
+
sign = jnp.asarray(self.sign, dtype=jnp.float64)
|
|
1687
|
+
shape = owner_i.shape
|
|
1688
|
+
for name, value in (
|
|
1689
|
+
("owner_j", owner_j),
|
|
1690
|
+
("owner_k", owner_k),
|
|
1691
|
+
("center", center),
|
|
1692
|
+
("normal_contra", normal_contra),
|
|
1693
|
+
("area_covector", area_covector),
|
|
1694
|
+
("distance", distance),
|
|
1695
|
+
("J", J),
|
|
1696
|
+
("g_contra", g_contra),
|
|
1697
|
+
("g_cov", g_cov),
|
|
1698
|
+
("B_contra", B_contra),
|
|
1699
|
+
("Bmag", Bmag),
|
|
1700
|
+
("sign", sign),
|
|
1701
|
+
):
|
|
1702
|
+
if name in {"center", "normal_contra", "area_covector", "B_contra"}:
|
|
1703
|
+
expected = shape + (3,)
|
|
1704
|
+
elif name in {"g_contra", "g_cov"}:
|
|
1705
|
+
expected = shape + (3, 3)
|
|
1706
|
+
else:
|
|
1707
|
+
expected = shape
|
|
1708
|
+
if value.shape != expected:
|
|
1709
|
+
raise ValueError(f"CutWallGeometry3D.{name} must have shape {expected}, got {value.shape}")
|
|
1710
|
+
object.__setattr__(self, "owner_i", owner_i)
|
|
1711
|
+
object.__setattr__(self, "owner_j", owner_j)
|
|
1712
|
+
object.__setattr__(self, "owner_k", owner_k)
|
|
1713
|
+
object.__setattr__(self, "center", center)
|
|
1714
|
+
object.__setattr__(self, "normal_contra", normal_contra)
|
|
1715
|
+
object.__setattr__(self, "area_covector", area_covector)
|
|
1716
|
+
object.__setattr__(self, "distance", distance)
|
|
1717
|
+
object.__setattr__(self, "J", J)
|
|
1718
|
+
object.__setattr__(self, "g_contra", g_contra)
|
|
1719
|
+
object.__setattr__(self, "g_cov", g_cov)
|
|
1720
|
+
object.__setattr__(self, "B_contra", B_contra)
|
|
1721
|
+
object.__setattr__(self, "Bmag", Bmag)
|
|
1722
|
+
object.__setattr__(self, "sign", sign)
|
|
1723
|
+
|
|
1724
|
+
@property
|
|
1725
|
+
def n_wall_faces(self) -> int:
|
|
1726
|
+
return int(self.owner_i.size)
|
|
1727
|
+
|
|
1728
|
+
@classmethod
|
|
1729
|
+
def empty(cls) -> "CutWallGeometry3D":
|
|
1730
|
+
return cls(
|
|
1731
|
+
owner_i=jnp.zeros((0,), dtype=jnp.int32),
|
|
1732
|
+
owner_j=jnp.zeros((0,), dtype=jnp.int32),
|
|
1733
|
+
owner_k=jnp.zeros((0,), dtype=jnp.int32),
|
|
1734
|
+
center=jnp.zeros((0, 3), dtype=jnp.float64),
|
|
1735
|
+
normal_contra=jnp.zeros((0, 3), dtype=jnp.float64),
|
|
1736
|
+
area_covector=jnp.zeros((0, 3), dtype=jnp.float64),
|
|
1737
|
+
distance=jnp.zeros((0,), dtype=jnp.float64),
|
|
1738
|
+
J=jnp.zeros((0,), dtype=jnp.float64),
|
|
1739
|
+
g_contra=jnp.zeros((0, 3, 3), dtype=jnp.float64),
|
|
1740
|
+
g_cov=jnp.zeros((0, 3, 3), dtype=jnp.float64),
|
|
1741
|
+
B_contra=jnp.zeros((0, 3), dtype=jnp.float64),
|
|
1742
|
+
Bmag=jnp.zeros((0,), dtype=jnp.float64),
|
|
1743
|
+
sign=jnp.zeros((0,), dtype=jnp.float64),
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
def tree_flatten(self):
|
|
1747
|
+
return (
|
|
1748
|
+
(
|
|
1749
|
+
self.owner_i,
|
|
1750
|
+
self.owner_j,
|
|
1751
|
+
self.owner_k,
|
|
1752
|
+
self.center,
|
|
1753
|
+
self.normal_contra,
|
|
1754
|
+
self.area_covector,
|
|
1755
|
+
self.distance,
|
|
1756
|
+
self.J,
|
|
1757
|
+
self.g_contra,
|
|
1758
|
+
self.g_cov,
|
|
1759
|
+
self.B_contra,
|
|
1760
|
+
self.Bmag,
|
|
1761
|
+
self.sign,
|
|
1762
|
+
),
|
|
1763
|
+
None,
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
@classmethod
|
|
1767
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1768
|
+
return cls(*children)
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
@_pytree_base
|
|
1772
|
+
@dataclass(frozen=True)
|
|
1773
|
+
class CutWallBC3D:
|
|
1774
|
+
"""Future embedded-wall boundary data."""
|
|
1775
|
+
|
|
1776
|
+
kind: jnp.ndarray # (n_cut_faces,)
|
|
1777
|
+
value: jnp.ndarray # (n_cut_faces,)
|
|
1778
|
+
|
|
1779
|
+
def __post_init__(self) -> None:
|
|
1780
|
+
kind = jnp.asarray(self.kind, dtype=jnp.int32)
|
|
1781
|
+
value = jnp.asarray(self.value, dtype=jnp.float64)
|
|
1782
|
+
if kind.ndim != 1 or value.ndim != 1:
|
|
1783
|
+
raise ValueError("CutWallBC3D fields must be one-dimensional")
|
|
1784
|
+
if kind.shape != value.shape:
|
|
1785
|
+
raise ValueError(f"CutWallBC3D.kind and value must have the same shape, got {kind.shape} and {value.shape}")
|
|
1786
|
+
object.__setattr__(self, "kind", kind)
|
|
1787
|
+
object.__setattr__(self, "value", value)
|
|
1788
|
+
|
|
1789
|
+
@property
|
|
1790
|
+
def n_wall_faces(self) -> int:
|
|
1791
|
+
return int(self.kind.size)
|
|
1792
|
+
|
|
1793
|
+
@classmethod
|
|
1794
|
+
def empty(cls) -> "CutWallBC3D":
|
|
1795
|
+
return cls(
|
|
1796
|
+
kind=jnp.zeros((0,), dtype=jnp.int32),
|
|
1797
|
+
value=jnp.zeros((0,), dtype=jnp.float64),
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
def tree_flatten(self):
|
|
1801
|
+
return ((self.kind, self.value), None)
|
|
1802
|
+
|
|
1803
|
+
@classmethod
|
|
1804
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1805
|
+
return cls(*children)
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
@dataclass(frozen=True)
|
|
1809
|
+
class FourFieldBoundaryConditions:
|
|
1810
|
+
"""Per-field face BCs (plus optional cut-wall data) for the four-field model."""
|
|
1811
|
+
|
|
1812
|
+
phi_face_bc: BoundaryFaceBC3D
|
|
1813
|
+
density_face_bc: BoundaryFaceBC3D
|
|
1814
|
+
omega_face_bc: BoundaryFaceBC3D
|
|
1815
|
+
v_ion_parallel_face_bc: BoundaryFaceBC3D
|
|
1816
|
+
v_electron_parallel_face_bc: BoundaryFaceBC3D
|
|
1817
|
+
phi_cut_wall_geometry: CutWallGeometry3D = field(default_factory=CutWallGeometry3D.empty)
|
|
1818
|
+
phi_cut_wall_bc: CutWallBC3D = field(default_factory=CutWallBC3D.empty)
|
|
1819
|
+
density_cut_wall_geometry: CutWallGeometry3D = field(default_factory=CutWallGeometry3D.empty)
|
|
1820
|
+
density_cut_wall_bc: CutWallBC3D = field(default_factory=CutWallBC3D.empty)
|
|
1821
|
+
omega_cut_wall_geometry: CutWallGeometry3D = field(default_factory=CutWallGeometry3D.empty)
|
|
1822
|
+
omega_cut_wall_bc: CutWallBC3D = field(default_factory=CutWallBC3D.empty)
|
|
1823
|
+
v_ion_parallel_cut_wall_geometry: CutWallGeometry3D = field(default_factory=CutWallGeometry3D.empty)
|
|
1824
|
+
v_ion_parallel_cut_wall_bc: CutWallBC3D = field(default_factory=CutWallBC3D.empty)
|
|
1825
|
+
v_electron_parallel_cut_wall_geometry: CutWallGeometry3D = field(default_factory=CutWallGeometry3D.empty)
|
|
1826
|
+
v_electron_parallel_cut_wall_bc: CutWallBC3D = field(default_factory=CutWallBC3D.empty)
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
@_pytree_base
|
|
1830
|
+
@dataclass(frozen=True)
|
|
1831
|
+
class LocalCutWallGeometry3D(_DataclassPyTreeMixin):
|
|
1832
|
+
"""Padded cut-wall geometry owned by one local shard."""
|
|
1833
|
+
|
|
1834
|
+
owner_i: jnp.ndarray
|
|
1835
|
+
owner_j: jnp.ndarray
|
|
1836
|
+
owner_k: jnp.ndarray
|
|
1837
|
+
center: jnp.ndarray
|
|
1838
|
+
normal_contra: jnp.ndarray
|
|
1839
|
+
area_covector: jnp.ndarray
|
|
1840
|
+
distance: jnp.ndarray
|
|
1841
|
+
J: jnp.ndarray
|
|
1842
|
+
g_contra: jnp.ndarray
|
|
1843
|
+
g_cov: jnp.ndarray
|
|
1844
|
+
B_contra: jnp.ndarray
|
|
1845
|
+
Bmag: jnp.ndarray
|
|
1846
|
+
sign: jnp.ndarray
|
|
1847
|
+
active: jnp.ndarray
|
|
1848
|
+
max_wall_faces: int
|
|
1849
|
+
|
|
1850
|
+
def __post_init__(self) -> None:
|
|
1851
|
+
max_wall_faces = int(self.max_wall_faces)
|
|
1852
|
+
if max_wall_faces < 0:
|
|
1853
|
+
raise ValueError(f"max_wall_faces must be non-negative, got {max_wall_faces}")
|
|
1854
|
+
|
|
1855
|
+
owner_i = _as_local_wall_int_array(self.owner_i, max_wall_faces, "LocalCutWallGeometry3D.owner_i")
|
|
1856
|
+
owner_j = _as_local_wall_int_array(self.owner_j, max_wall_faces, "LocalCutWallGeometry3D.owner_j")
|
|
1857
|
+
owner_k = _as_local_wall_int_array(self.owner_k, max_wall_faces, "LocalCutWallGeometry3D.owner_k")
|
|
1858
|
+
center = _as_local_wall_array(self.center, max_wall_faces, (3,), "LocalCutWallGeometry3D.center")
|
|
1859
|
+
normal_contra = _as_local_wall_array(self.normal_contra, max_wall_faces, (3,), "LocalCutWallGeometry3D.normal_contra")
|
|
1860
|
+
area_covector = _as_local_wall_array(self.area_covector, max_wall_faces, (3,), "LocalCutWallGeometry3D.area_covector")
|
|
1861
|
+
distance = _as_local_wall_array(self.distance, max_wall_faces, (), "LocalCutWallGeometry3D.distance")
|
|
1862
|
+
J = _as_local_wall_array(self.J, max_wall_faces, (), "LocalCutWallGeometry3D.J")
|
|
1863
|
+
g_contra = _as_local_wall_array(self.g_contra, max_wall_faces, (3, 3), "LocalCutWallGeometry3D.g_contra")
|
|
1864
|
+
g_cov = _as_local_wall_array(self.g_cov, max_wall_faces, (3, 3), "LocalCutWallGeometry3D.g_cov")
|
|
1865
|
+
B_contra = _as_local_wall_array(self.B_contra, max_wall_faces, (3,), "LocalCutWallGeometry3D.B_contra")
|
|
1866
|
+
Bmag = _as_local_wall_array(self.Bmag, max_wall_faces, (), "LocalCutWallGeometry3D.Bmag")
|
|
1867
|
+
sign = _as_local_wall_array(self.sign, max_wall_faces, (), "LocalCutWallGeometry3D.sign")
|
|
1868
|
+
active = _as_local_wall_bool_array(self.active, max_wall_faces, "LocalCutWallGeometry3D.active")
|
|
1869
|
+
|
|
1870
|
+
object.__setattr__(self, "owner_i", owner_i)
|
|
1871
|
+
object.__setattr__(self, "owner_j", owner_j)
|
|
1872
|
+
object.__setattr__(self, "owner_k", owner_k)
|
|
1873
|
+
object.__setattr__(self, "center", center)
|
|
1874
|
+
object.__setattr__(self, "normal_contra", normal_contra)
|
|
1875
|
+
object.__setattr__(self, "area_covector", area_covector)
|
|
1876
|
+
object.__setattr__(self, "distance", distance)
|
|
1877
|
+
object.__setattr__(self, "J", J)
|
|
1878
|
+
object.__setattr__(self, "g_contra", g_contra)
|
|
1879
|
+
object.__setattr__(self, "g_cov", g_cov)
|
|
1880
|
+
object.__setattr__(self, "B_contra", B_contra)
|
|
1881
|
+
object.__setattr__(self, "Bmag", Bmag)
|
|
1882
|
+
object.__setattr__(self, "sign", sign)
|
|
1883
|
+
object.__setattr__(self, "active", active)
|
|
1884
|
+
object.__setattr__(self, "max_wall_faces", max_wall_faces)
|
|
1885
|
+
|
|
1886
|
+
@property
|
|
1887
|
+
def n_wall_faces(self) -> int:
|
|
1888
|
+
return int(self.max_wall_faces)
|
|
1889
|
+
|
|
1890
|
+
@classmethod
|
|
1891
|
+
def empty(cls, max_wall_faces: int) -> "LocalCutWallGeometry3D":
|
|
1892
|
+
max_wall_faces = int(max_wall_faces)
|
|
1893
|
+
zeros3 = (max_wall_faces, 3)
|
|
1894
|
+
zeros33 = (max_wall_faces, 3, 3)
|
|
1895
|
+
return cls(
|
|
1896
|
+
owner_i=jnp.zeros((max_wall_faces,), dtype=jnp.int32),
|
|
1897
|
+
owner_j=jnp.zeros((max_wall_faces,), dtype=jnp.int32),
|
|
1898
|
+
owner_k=jnp.zeros((max_wall_faces,), dtype=jnp.int32),
|
|
1899
|
+
center=jnp.zeros(zeros3, dtype=jnp.float64),
|
|
1900
|
+
normal_contra=jnp.zeros(zeros3, dtype=jnp.float64),
|
|
1901
|
+
area_covector=jnp.zeros(zeros3, dtype=jnp.float64),
|
|
1902
|
+
distance=jnp.zeros((max_wall_faces,), dtype=jnp.float64),
|
|
1903
|
+
J=jnp.zeros((max_wall_faces,), dtype=jnp.float64),
|
|
1904
|
+
g_contra=jnp.zeros(zeros33, dtype=jnp.float64),
|
|
1905
|
+
g_cov=jnp.zeros(zeros33, dtype=jnp.float64),
|
|
1906
|
+
B_contra=jnp.zeros(zeros3, dtype=jnp.float64),
|
|
1907
|
+
Bmag=jnp.zeros((max_wall_faces,), dtype=jnp.float64),
|
|
1908
|
+
sign=jnp.zeros((max_wall_faces,), dtype=jnp.float64),
|
|
1909
|
+
active=jnp.zeros((max_wall_faces,), dtype=bool),
|
|
1910
|
+
max_wall_faces=max_wall_faces,
|
|
1911
|
+
)
|
|
1912
|
+
|
|
1913
|
+
def tree_flatten(self):
|
|
1914
|
+
return (
|
|
1915
|
+
(
|
|
1916
|
+
self.owner_i,
|
|
1917
|
+
self.owner_j,
|
|
1918
|
+
self.owner_k,
|
|
1919
|
+
self.center,
|
|
1920
|
+
self.normal_contra,
|
|
1921
|
+
self.area_covector,
|
|
1922
|
+
self.distance,
|
|
1923
|
+
self.J,
|
|
1924
|
+
self.g_contra,
|
|
1925
|
+
self.g_cov,
|
|
1926
|
+
self.B_contra,
|
|
1927
|
+
self.Bmag,
|
|
1928
|
+
self.sign,
|
|
1929
|
+
self.active,
|
|
1930
|
+
),
|
|
1931
|
+
self.max_wall_faces,
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
@classmethod
|
|
1935
|
+
def tree_unflatten(cls, max_wall_faces, children):
|
|
1936
|
+
(
|
|
1937
|
+
owner_i,
|
|
1938
|
+
owner_j,
|
|
1939
|
+
owner_k,
|
|
1940
|
+
center,
|
|
1941
|
+
normal_contra,
|
|
1942
|
+
area_covector,
|
|
1943
|
+
distance,
|
|
1944
|
+
J,
|
|
1945
|
+
g_contra,
|
|
1946
|
+
g_cov,
|
|
1947
|
+
B_contra,
|
|
1948
|
+
Bmag,
|
|
1949
|
+
sign,
|
|
1950
|
+
active,
|
|
1951
|
+
) = children
|
|
1952
|
+
return cls(
|
|
1953
|
+
owner_i=owner_i,
|
|
1954
|
+
owner_j=owner_j,
|
|
1955
|
+
owner_k=owner_k,
|
|
1956
|
+
center=center,
|
|
1957
|
+
normal_contra=normal_contra,
|
|
1958
|
+
area_covector=area_covector,
|
|
1959
|
+
distance=distance,
|
|
1960
|
+
J=J,
|
|
1961
|
+
g_contra=g_contra,
|
|
1962
|
+
g_cov=g_cov,
|
|
1963
|
+
B_contra=B_contra,
|
|
1964
|
+
Bmag=Bmag,
|
|
1965
|
+
sign=sign,
|
|
1966
|
+
active=active,
|
|
1967
|
+
max_wall_faces=max_wall_faces,
|
|
1968
|
+
)
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
@_pytree_base
|
|
1972
|
+
@dataclass(frozen=True)
|
|
1973
|
+
class LocalCutWallBC3D(_DataclassPyTreeMixin):
|
|
1974
|
+
"""
|
|
1975
|
+
Local padded cut-wall boundary-condition payload for one shard.
|
|
1976
|
+
|
|
1977
|
+
Each active entry corresponds to one active entry in LocalCutWallGeometry3D.
|
|
1978
|
+
|
|
1979
|
+
The leading dimension is Wmax, where:
|
|
1980
|
+
|
|
1981
|
+
Wmax = maximum number of cut-wall pieces per shard/local domain
|
|
1982
|
+
|
|
1983
|
+
not the total global number of cut-wall pieces.
|
|
1984
|
+
|
|
1985
|
+
The BC arrays are padded so all shards have the same static shape.
|
|
1986
|
+
Inactive padded entries are masked by active=False.
|
|
1987
|
+
"""
|
|
1988
|
+
|
|
1989
|
+
kind: jnp.ndarray
|
|
1990
|
+
value: jnp.ndarray
|
|
1991
|
+
active: jnp.ndarray
|
|
1992
|
+
max_wall_faces: int
|
|
1993
|
+
|
|
1994
|
+
def __post_init__(self) -> None:
|
|
1995
|
+
max_wall_faces = int(self.max_wall_faces)
|
|
1996
|
+
if max_wall_faces < 0:
|
|
1997
|
+
raise ValueError(f"max_wall_faces must be non-negative, got {max_wall_faces}")
|
|
1998
|
+
|
|
1999
|
+
kind = jnp.asarray(self.kind, dtype=jnp.int32)
|
|
2000
|
+
value = jnp.asarray(self.value, dtype=jnp.float64)
|
|
2001
|
+
active = jnp.asarray(self.active, dtype=bool)
|
|
2002
|
+
expected = (max_wall_faces,)
|
|
2003
|
+
if kind.shape != expected:
|
|
2004
|
+
raise ValueError(f"LocalCutWallBC3D.kind must have shape {expected}, got {kind.shape}")
|
|
2005
|
+
if value.shape != expected:
|
|
2006
|
+
raise ValueError(f"LocalCutWallBC3D.value must have shape {expected}, got {value.shape}")
|
|
2007
|
+
if active.shape != expected:
|
|
2008
|
+
raise ValueError(f"LocalCutWallBC3D.active must have shape {expected}, got {active.shape}")
|
|
2009
|
+
|
|
2010
|
+
kind = jnp.where(active, kind, BC_NONE)
|
|
2011
|
+
value = jnp.where(active, value, 0.0)
|
|
2012
|
+
object.__setattr__(self, "kind", kind)
|
|
2013
|
+
object.__setattr__(self, "value", value)
|
|
2014
|
+
object.__setattr__(self, "active", active)
|
|
2015
|
+
object.__setattr__(self, "max_wall_faces", max_wall_faces)
|
|
2016
|
+
|
|
2017
|
+
@property
|
|
2018
|
+
def shape(self) -> tuple[int]:
|
|
2019
|
+
return (int(self.max_wall_faces),)
|
|
2020
|
+
|
|
2021
|
+
@property
|
|
2022
|
+
def n_wall_faces(self) -> int:
|
|
2023
|
+
return int(self.max_wall_faces)
|
|
2024
|
+
|
|
2025
|
+
@property
|
|
2026
|
+
def n_active(self) -> jnp.ndarray:
|
|
2027
|
+
return jnp.sum(self.active)
|
|
2028
|
+
|
|
2029
|
+
@classmethod
|
|
2030
|
+
def empty(cls, max_wall_faces: int) -> "LocalCutWallBC3D":
|
|
2031
|
+
max_wall_faces = int(max_wall_faces)
|
|
2032
|
+
return cls(
|
|
2033
|
+
kind=jnp.zeros((max_wall_faces,), dtype=jnp.int32),
|
|
2034
|
+
value=jnp.zeros((max_wall_faces,), dtype=jnp.float64),
|
|
2035
|
+
active=jnp.zeros((max_wall_faces,), dtype=bool),
|
|
2036
|
+
max_wall_faces=max_wall_faces,
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
def tree_flatten(self):
|
|
2040
|
+
return ((self.kind, self.value, self.active), self.max_wall_faces)
|
|
2041
|
+
|
|
2042
|
+
@classmethod
|
|
2043
|
+
def tree_unflatten(cls, max_wall_faces, children):
|
|
2044
|
+
kind, value, active = children
|
|
2045
|
+
return cls(kind=kind, value=value, active=active, max_wall_faces=max_wall_faces)
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
@_pytree_base
|
|
2049
|
+
@dataclass(frozen=True)
|
|
2050
|
+
class LocalCutWallValueReconstructor3D(_DataclassPyTreeMixin):
|
|
2051
|
+
"""Reconstruct field values at local cut-wall pieces from field_halo."""
|
|
2052
|
+
|
|
2053
|
+
cut_wall_geometry: LocalCutWallGeometry3D
|
|
2054
|
+
neighbor_i: jnp.ndarray
|
|
2055
|
+
neighbor_j: jnp.ndarray
|
|
2056
|
+
neighbor_k: jnp.ndarray
|
|
2057
|
+
weights: jnp.ndarray
|
|
2058
|
+
active: jnp.ndarray
|
|
2059
|
+
stencil_width: int
|
|
2060
|
+
max_wall_faces: int
|
|
2061
|
+
|
|
2062
|
+
def __post_init__(self) -> None:
|
|
2063
|
+
if not isinstance(self.cut_wall_geometry, LocalCutWallGeometry3D):
|
|
2064
|
+
raise TypeError("cut_wall_geometry must be a LocalCutWallGeometry3D")
|
|
2065
|
+
|
|
2066
|
+
max_wall_faces = int(self.max_wall_faces)
|
|
2067
|
+
stencil_width = int(self.stencil_width)
|
|
2068
|
+
if max_wall_faces < 0:
|
|
2069
|
+
raise ValueError(f"max_wall_faces must be non-negative, got {max_wall_faces}")
|
|
2070
|
+
if stencil_width < 1:
|
|
2071
|
+
raise ValueError(f"stencil_width must be positive, got {stencil_width}")
|
|
2072
|
+
if self.cut_wall_geometry.max_wall_faces != max_wall_faces:
|
|
2073
|
+
raise ValueError(
|
|
2074
|
+
"LocalCutWallValueReconstructor3D.cut_wall_geometry.max_wall_faces must match max_wall_faces; "
|
|
2075
|
+
f"got {self.cut_wall_geometry.max_wall_faces} and {max_wall_faces}"
|
|
2076
|
+
)
|
|
2077
|
+
|
|
2078
|
+
expected_stencil = (max_wall_faces, stencil_width)
|
|
2079
|
+
expected_wall = (max_wall_faces,)
|
|
2080
|
+
neighbor_i = _as_local_wall_stencil_index_array(
|
|
2081
|
+
self.neighbor_i, max_wall_faces, stencil_width, "LocalCutWallValueReconstructor3D.neighbor_i"
|
|
2082
|
+
)
|
|
2083
|
+
neighbor_j = _as_local_wall_stencil_index_array(
|
|
2084
|
+
self.neighbor_j, max_wall_faces, stencil_width, "LocalCutWallValueReconstructor3D.neighbor_j"
|
|
2085
|
+
)
|
|
2086
|
+
neighbor_k = _as_local_wall_stencil_index_array(
|
|
2087
|
+
self.neighbor_k, max_wall_faces, stencil_width, "LocalCutWallValueReconstructor3D.neighbor_k"
|
|
2088
|
+
)
|
|
2089
|
+
weights = _as_local_wall_stencil_weight_array(
|
|
2090
|
+
self.weights, max_wall_faces, stencil_width, "LocalCutWallValueReconstructor3D.weights"
|
|
2091
|
+
)
|
|
2092
|
+
active = _as_local_wall_bool_array(self.active, max_wall_faces, "LocalCutWallValueReconstructor3D.active")
|
|
2093
|
+
|
|
2094
|
+
if active.shape != expected_wall:
|
|
2095
|
+
raise ValueError(
|
|
2096
|
+
f"LocalCutWallValueReconstructor3D.active must have shape {expected_wall}, got {active.shape}"
|
|
2097
|
+
)
|
|
2098
|
+
|
|
2099
|
+
active2 = active[:, None]
|
|
2100
|
+
neighbor_i = jnp.where(active2, neighbor_i, 0)
|
|
2101
|
+
neighbor_j = jnp.where(active2, neighbor_j, 0)
|
|
2102
|
+
neighbor_k = jnp.where(active2, neighbor_k, 0)
|
|
2103
|
+
weights = jnp.where(active2, weights, 0.0)
|
|
2104
|
+
|
|
2105
|
+
object.__setattr__(self, "neighbor_i", neighbor_i)
|
|
2106
|
+
object.__setattr__(self, "neighbor_j", neighbor_j)
|
|
2107
|
+
object.__setattr__(self, "neighbor_k", neighbor_k)
|
|
2108
|
+
object.__setattr__(self, "weights", weights)
|
|
2109
|
+
object.__setattr__(self, "active", active)
|
|
2110
|
+
object.__setattr__(self, "stencil_width", stencil_width)
|
|
2111
|
+
object.__setattr__(self, "max_wall_faces", max_wall_faces)
|
|
2112
|
+
|
|
2113
|
+
def extrapolate(self, field_halo: jnp.ndarray) -> jnp.ndarray:
|
|
2114
|
+
values = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
2115
|
+
gathered = values[self.neighbor_i, self.neighbor_j, self.neighbor_k]
|
|
2116
|
+
wall_value = jnp.sum(self.weights * gathered, axis=-1)
|
|
2117
|
+
return jnp.where(self.active, wall_value, 0.0)
|
|
2118
|
+
|
|
2119
|
+
def tree_flatten(self):
|
|
2120
|
+
return (
|
|
2121
|
+
(
|
|
2122
|
+
self.cut_wall_geometry,
|
|
2123
|
+
self.neighbor_i,
|
|
2124
|
+
self.neighbor_j,
|
|
2125
|
+
self.neighbor_k,
|
|
2126
|
+
self.weights,
|
|
2127
|
+
self.active,
|
|
2128
|
+
),
|
|
2129
|
+
(self.stencil_width, self.max_wall_faces),
|
|
2130
|
+
)
|
|
2131
|
+
|
|
2132
|
+
@classmethod
|
|
2133
|
+
def tree_unflatten(cls, aux_data, children):
|
|
2134
|
+
stencil_width, max_wall_faces = aux_data
|
|
2135
|
+
(
|
|
2136
|
+
cut_wall_geometry,
|
|
2137
|
+
neighbor_i,
|
|
2138
|
+
neighbor_j,
|
|
2139
|
+
neighbor_k,
|
|
2140
|
+
weights,
|
|
2141
|
+
active,
|
|
2142
|
+
) = children
|
|
2143
|
+
return cls(
|
|
2144
|
+
cut_wall_geometry=cut_wall_geometry,
|
|
2145
|
+
neighbor_i=neighbor_i,
|
|
2146
|
+
neighbor_j=neighbor_j,
|
|
2147
|
+
neighbor_k=neighbor_k,
|
|
2148
|
+
weights=weights,
|
|
2149
|
+
active=active,
|
|
2150
|
+
stencil_width=stencil_width,
|
|
2151
|
+
max_wall_faces=max_wall_faces,
|
|
2152
|
+
)
|
|
2153
|
+
|
|
2154
|
+
|
|
2155
|
+
@_pytree_base
|
|
2156
|
+
@dataclass(frozen=True)
|
|
2157
|
+
class LocalCutWallNormalDerivativeConstructor3D(_DataclassPyTreeMixin):
|
|
2158
|
+
"""Construct wall-normal derivatives at local cut-wall pieces from field_halo."""
|
|
2159
|
+
|
|
2160
|
+
cut_wall_geometry: LocalCutWallGeometry3D
|
|
2161
|
+
neighbor_i: jnp.ndarray
|
|
2162
|
+
neighbor_j: jnp.ndarray
|
|
2163
|
+
neighbor_k: jnp.ndarray
|
|
2164
|
+
weights_dnormal: jnp.ndarray
|
|
2165
|
+
weights_d2normal: jnp.ndarray
|
|
2166
|
+
wall_coeff_dnormal: jnp.ndarray
|
|
2167
|
+
wall_coeff_d2normal: jnp.ndarray
|
|
2168
|
+
active: jnp.ndarray
|
|
2169
|
+
stencil_width: int
|
|
2170
|
+
max_wall_faces: int
|
|
2171
|
+
|
|
2172
|
+
def __post_init__(self) -> None:
|
|
2173
|
+
if not isinstance(self.cut_wall_geometry, LocalCutWallGeometry3D):
|
|
2174
|
+
raise TypeError("cut_wall_geometry must be a LocalCutWallGeometry3D")
|
|
2175
|
+
|
|
2176
|
+
max_wall_faces = int(self.max_wall_faces)
|
|
2177
|
+
stencil_width = int(self.stencil_width)
|
|
2178
|
+
if max_wall_faces < 0:
|
|
2179
|
+
raise ValueError(f"max_wall_faces must be non-negative, got {max_wall_faces}")
|
|
2180
|
+
if stencil_width < 1:
|
|
2181
|
+
raise ValueError(f"stencil_width must be positive, got {stencil_width}")
|
|
2182
|
+
if self.cut_wall_geometry.max_wall_faces != max_wall_faces:
|
|
2183
|
+
raise ValueError(
|
|
2184
|
+
"LocalCutWallNormalDerivativeConstructor3D.cut_wall_geometry.max_wall_faces must match max_wall_faces; "
|
|
2185
|
+
f"got {self.cut_wall_geometry.max_wall_faces} and {max_wall_faces}"
|
|
2186
|
+
)
|
|
2187
|
+
|
|
2188
|
+
expected_stencil = (max_wall_faces, stencil_width)
|
|
2189
|
+
expected_wall = (max_wall_faces,)
|
|
2190
|
+
neighbor_i = _as_local_wall_stencil_index_array(
|
|
2191
|
+
self.neighbor_i, max_wall_faces, stencil_width, "LocalCutWallNormalDerivativeConstructor3D.neighbor_i"
|
|
2192
|
+
)
|
|
2193
|
+
neighbor_j = _as_local_wall_stencil_index_array(
|
|
2194
|
+
self.neighbor_j, max_wall_faces, stencil_width, "LocalCutWallNormalDerivativeConstructor3D.neighbor_j"
|
|
2195
|
+
)
|
|
2196
|
+
neighbor_k = _as_local_wall_stencil_index_array(
|
|
2197
|
+
self.neighbor_k, max_wall_faces, stencil_width, "LocalCutWallNormalDerivativeConstructor3D.neighbor_k"
|
|
2198
|
+
)
|
|
2199
|
+
weights_dnormal = _as_local_wall_stencil_weight_array(
|
|
2200
|
+
self.weights_dnormal, max_wall_faces, stencil_width, "LocalCutWallNormalDerivativeConstructor3D.weights_dnormal"
|
|
2201
|
+
)
|
|
2202
|
+
weights_d2normal = _as_local_wall_stencil_weight_array(
|
|
2203
|
+
self.weights_d2normal, max_wall_faces, stencil_width, "LocalCutWallNormalDerivativeConstructor3D.weights_d2normal"
|
|
2204
|
+
)
|
|
2205
|
+
wall_coeff_dnormal = _as_local_wall_array(
|
|
2206
|
+
self.wall_coeff_dnormal, max_wall_faces, (), "LocalCutWallNormalDerivativeConstructor3D.wall_coeff_dnormal"
|
|
2207
|
+
)
|
|
2208
|
+
wall_coeff_d2normal = _as_local_wall_array(
|
|
2209
|
+
self.wall_coeff_d2normal, max_wall_faces, (), "LocalCutWallNormalDerivativeConstructor3D.wall_coeff_d2normal"
|
|
2210
|
+
)
|
|
2211
|
+
active = _as_local_wall_bool_array(self.active, max_wall_faces, "LocalCutWallNormalDerivativeConstructor3D.active")
|
|
2212
|
+
|
|
2213
|
+
if active.shape != expected_wall:
|
|
2214
|
+
raise ValueError(
|
|
2215
|
+
f"LocalCutWallNormalDerivativeConstructor3D.active must have shape {expected_wall}, got {active.shape}"
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
active2 = active[:, None]
|
|
2219
|
+
neighbor_i = jnp.where(active2, neighbor_i, 0)
|
|
2220
|
+
neighbor_j = jnp.where(active2, neighbor_j, 0)
|
|
2221
|
+
neighbor_k = jnp.where(active2, neighbor_k, 0)
|
|
2222
|
+
weights_dnormal = jnp.where(active2, weights_dnormal, 0.0)
|
|
2223
|
+
weights_d2normal = jnp.where(active2, weights_d2normal, 0.0)
|
|
2224
|
+
wall_coeff_dnormal = jnp.where(active, wall_coeff_dnormal, 0.0)
|
|
2225
|
+
wall_coeff_d2normal = jnp.where(active, wall_coeff_d2normal, 0.0)
|
|
2226
|
+
|
|
2227
|
+
object.__setattr__(self, "neighbor_i", neighbor_i)
|
|
2228
|
+
object.__setattr__(self, "neighbor_j", neighbor_j)
|
|
2229
|
+
object.__setattr__(self, "neighbor_k", neighbor_k)
|
|
2230
|
+
object.__setattr__(self, "weights_dnormal", weights_dnormal)
|
|
2231
|
+
object.__setattr__(self, "weights_d2normal", weights_d2normal)
|
|
2232
|
+
object.__setattr__(self, "wall_coeff_dnormal", wall_coeff_dnormal)
|
|
2233
|
+
object.__setattr__(self, "wall_coeff_d2normal", wall_coeff_d2normal)
|
|
2234
|
+
object.__setattr__(self, "active", active)
|
|
2235
|
+
object.__setattr__(self, "stencil_width", stencil_width)
|
|
2236
|
+
object.__setattr__(self, "max_wall_faces", max_wall_faces)
|
|
2237
|
+
|
|
2238
|
+
def _gather(self, field_halo: jnp.ndarray) -> jnp.ndarray:
|
|
2239
|
+
values = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
2240
|
+
return values[self.neighbor_i, self.neighbor_j, self.neighbor_k]
|
|
2241
|
+
|
|
2242
|
+
def dnormal_from_wall_value(self, field_halo: jnp.ndarray, wall_value: jnp.ndarray) -> jnp.ndarray:
|
|
2243
|
+
wall_value = jnp.asarray(wall_value, dtype=jnp.float64)
|
|
2244
|
+
expected_wall = (int(self.max_wall_faces),)
|
|
2245
|
+
if wall_value.shape != expected_wall:
|
|
2246
|
+
raise ValueError(f"wall_value must have shape {expected_wall}, got {wall_value.shape}")
|
|
2247
|
+
gathered = self._gather(field_halo)
|
|
2248
|
+
dnormal = jnp.sum(self.weights_dnormal * gathered, axis=-1) + self.wall_coeff_dnormal * wall_value
|
|
2249
|
+
return jnp.where(self.active, dnormal, 0.0)
|
|
2250
|
+
|
|
2251
|
+
def d2normal_from_wall_value(self, field_halo: jnp.ndarray, wall_value: jnp.ndarray) -> jnp.ndarray:
|
|
2252
|
+
wall_value = jnp.asarray(wall_value, dtype=jnp.float64)
|
|
2253
|
+
expected_wall = (int(self.max_wall_faces),)
|
|
2254
|
+
if wall_value.shape != expected_wall:
|
|
2255
|
+
raise ValueError(f"wall_value must have shape {expected_wall}, got {wall_value.shape}")
|
|
2256
|
+
gathered = self._gather(field_halo)
|
|
2257
|
+
d2normal = jnp.sum(self.weights_d2normal * gathered, axis=-1) + self.wall_coeff_d2normal * wall_value
|
|
2258
|
+
return jnp.where(self.active, d2normal, 0.0)
|
|
2259
|
+
|
|
2260
|
+
def normal_derivatives_from_wall_value(
|
|
2261
|
+
self, field_halo: jnp.ndarray, wall_value: jnp.ndarray
|
|
2262
|
+
) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
2263
|
+
return (
|
|
2264
|
+
self.dnormal_from_wall_value(field_halo, wall_value),
|
|
2265
|
+
self.d2normal_from_wall_value(field_halo, wall_value),
|
|
2266
|
+
)
|
|
2267
|
+
|
|
2268
|
+
def tree_flatten(self):
|
|
2269
|
+
return (
|
|
2270
|
+
(
|
|
2271
|
+
self.cut_wall_geometry,
|
|
2272
|
+
self.neighbor_i,
|
|
2273
|
+
self.neighbor_j,
|
|
2274
|
+
self.neighbor_k,
|
|
2275
|
+
self.weights_dnormal,
|
|
2276
|
+
self.weights_d2normal,
|
|
2277
|
+
self.wall_coeff_dnormal,
|
|
2278
|
+
self.wall_coeff_d2normal,
|
|
2279
|
+
self.active,
|
|
2280
|
+
),
|
|
2281
|
+
(self.stencil_width, self.max_wall_faces),
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
@classmethod
|
|
2285
|
+
def tree_unflatten(cls, aux_data, children):
|
|
2286
|
+
stencil_width, max_wall_faces = aux_data
|
|
2287
|
+
(
|
|
2288
|
+
cut_wall_geometry,
|
|
2289
|
+
neighbor_i,
|
|
2290
|
+
neighbor_j,
|
|
2291
|
+
neighbor_k,
|
|
2292
|
+
weights_dnormal,
|
|
2293
|
+
weights_d2normal,
|
|
2294
|
+
wall_coeff_dnormal,
|
|
2295
|
+
wall_coeff_d2normal,
|
|
2296
|
+
active,
|
|
2297
|
+
) = children
|
|
2298
|
+
return cls(
|
|
2299
|
+
cut_wall_geometry=cut_wall_geometry,
|
|
2300
|
+
neighbor_i=neighbor_i,
|
|
2301
|
+
neighbor_j=neighbor_j,
|
|
2302
|
+
neighbor_k=neighbor_k,
|
|
2303
|
+
weights_dnormal=weights_dnormal,
|
|
2304
|
+
weights_d2normal=weights_d2normal,
|
|
2305
|
+
wall_coeff_dnormal=wall_coeff_dnormal,
|
|
2306
|
+
wall_coeff_d2normal=wall_coeff_d2normal,
|
|
2307
|
+
active=active,
|
|
2308
|
+
stencil_width=stencil_width,
|
|
2309
|
+
max_wall_faces=max_wall_faces,
|
|
2310
|
+
)
|
|
2311
|
+
|
|
2312
|
+
|
|
2313
|
+
@_pytree_base
|
|
2314
|
+
@dataclass(frozen=True)
|
|
2315
|
+
class LocalControlVolumeFluxStencil3D:
|
|
2316
|
+
"""Local control-volume flux payload consumed by conservative divergence."""
|
|
2317
|
+
|
|
2318
|
+
regular_flux: FaceFluxStencil3D
|
|
2319
|
+
regular_face_geometry: "LocalRegularFaceGeometry3D | RegularFaceGeometry3D"
|
|
2320
|
+
cell_volume: "LocalCellVolumeGeometry3D | CellVolumeGeometry3D"
|
|
2321
|
+
cut_wall_geometry: "LocalCutWallGeometry3D | CutWallGeometry3D | None" = None
|
|
2322
|
+
cut_wall_flux: jnp.ndarray | None = None
|
|
2323
|
+
|
|
2324
|
+
def __post_init__(self) -> None:
|
|
2325
|
+
if not isinstance(self.regular_flux, FaceFluxStencil3D):
|
|
2326
|
+
raise TypeError("LocalControlVolumeFluxStencil3D.regular_flux must be a FaceFluxStencil3D")
|
|
2327
|
+
if not isinstance(self.regular_face_geometry, (LocalRegularFaceGeometry3D, RegularFaceGeometry3D)):
|
|
2328
|
+
raise TypeError(
|
|
2329
|
+
"LocalControlVolumeFluxStencil3D.regular_face_geometry must be a "
|
|
2330
|
+
"LocalRegularFaceGeometry3D or RegularFaceGeometry3D"
|
|
2331
|
+
)
|
|
2332
|
+
if not isinstance(self.cell_volume, (LocalCellVolumeGeometry3D, CellVolumeGeometry3D)):
|
|
2333
|
+
raise TypeError(
|
|
2334
|
+
"LocalControlVolumeFluxStencil3D.cell_volume must be a "
|
|
2335
|
+
"LocalCellVolumeGeometry3D or CellVolumeGeometry3D"
|
|
2336
|
+
)
|
|
2337
|
+
cell_shape = self.cell_volume.shape
|
|
2338
|
+
if self.regular_flux.shape != cell_shape:
|
|
2339
|
+
raise ValueError(
|
|
2340
|
+
f"regular_flux.shape must match cell_volume.shape, got {self.regular_flux.shape} and {cell_shape}"
|
|
2341
|
+
)
|
|
2342
|
+
face_geometry_cell_shape = (
|
|
2343
|
+
self.regular_face_geometry.local_owned_shape
|
|
2344
|
+
if isinstance(self.regular_face_geometry, LocalRegularFaceGeometry3D)
|
|
2345
|
+
else self.regular_face_geometry.shape
|
|
2346
|
+
)
|
|
2347
|
+
if face_geometry_cell_shape != cell_shape:
|
|
2348
|
+
raise ValueError(
|
|
2349
|
+
"regular_face_geometry.local_owned_shape must match cell_volume.shape, "
|
|
2350
|
+
f"got {face_geometry_cell_shape} and {cell_shape}"
|
|
2351
|
+
)
|
|
2352
|
+
if (
|
|
2353
|
+
self.regular_face_geometry.x_area.shape != self.regular_flux.x.shape
|
|
2354
|
+
or self.regular_face_geometry.y_area.shape != self.regular_flux.y.shape
|
|
2355
|
+
or self.regular_face_geometry.z_area.shape != self.regular_flux.z.shape
|
|
2356
|
+
):
|
|
2357
|
+
raise ValueError(
|
|
2358
|
+
"regular_face_geometry face arrays must match regular_flux face shapes"
|
|
2359
|
+
)
|
|
2360
|
+
|
|
2361
|
+
if self.cut_wall_geometry is None:
|
|
2362
|
+
if self.cut_wall_flux is not None:
|
|
2363
|
+
raise ValueError("cut_wall_flux must be None when cut_wall_geometry is None")
|
|
2364
|
+
object.__setattr__(self, "cut_wall_flux", None)
|
|
2365
|
+
return
|
|
2366
|
+
|
|
2367
|
+
if not isinstance(self.cut_wall_geometry, (LocalCutWallGeometry3D, CutWallGeometry3D)):
|
|
2368
|
+
raise TypeError(
|
|
2369
|
+
"LocalControlVolumeFluxStencil3D.cut_wall_geometry must be a "
|
|
2370
|
+
"LocalCutWallGeometry3D or CutWallGeometry3D"
|
|
2371
|
+
)
|
|
2372
|
+
if self.cut_wall_flux is None:
|
|
2373
|
+
Wmax = getattr(self.cut_wall_geometry, "max_wall_faces", self.cut_wall_geometry.n_wall_faces)
|
|
2374
|
+
cut_wall_flux = jnp.zeros((Wmax,), dtype=jnp.float64)
|
|
2375
|
+
else:
|
|
2376
|
+
cut_wall_flux = jnp.asarray(self.cut_wall_flux, dtype=jnp.float64)
|
|
2377
|
+
expected = (self.cut_wall_geometry.n_wall_faces,)
|
|
2378
|
+
if cut_wall_flux.shape != expected:
|
|
2379
|
+
raise ValueError(
|
|
2380
|
+
f"LocalControlVolumeFluxStencil3D.cut_wall_flux must have shape {expected}, got {cut_wall_flux.shape}"
|
|
2381
|
+
)
|
|
2382
|
+
|
|
2383
|
+
cut_wall_active = getattr(self.cut_wall_geometry, "active", None)
|
|
2384
|
+
if cut_wall_active is not None:
|
|
2385
|
+
cut_wall_active = jnp.asarray(cut_wall_active, dtype=bool)
|
|
2386
|
+
if cut_wall_active.shape != cut_wall_flux.shape:
|
|
2387
|
+
raise ValueError(
|
|
2388
|
+
"LocalControlVolumeFluxStencil3D.cut_wall_geometry.active must match cut_wall_flux shape"
|
|
2389
|
+
)
|
|
2390
|
+
cut_wall_flux = jnp.where(cut_wall_active, cut_wall_flux, 0.0)
|
|
2391
|
+
object.__setattr__(self, "cut_wall_flux", cut_wall_flux)
|
|
2392
|
+
|
|
2393
|
+
@property
|
|
2394
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2395
|
+
return self.regular_flux.shape
|
|
2396
|
+
|
|
2397
|
+
def tree_flatten(self):
|
|
2398
|
+
return (
|
|
2399
|
+
(
|
|
2400
|
+
self.regular_flux,
|
|
2401
|
+
self.regular_face_geometry,
|
|
2402
|
+
self.cell_volume,
|
|
2403
|
+
self.cut_wall_geometry,
|
|
2404
|
+
self.cut_wall_flux,
|
|
2405
|
+
),
|
|
2406
|
+
None,
|
|
2407
|
+
)
|
|
2408
|
+
|
|
2409
|
+
@classmethod
|
|
2410
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
2411
|
+
return cls(*children)
|
|
2412
|
+
|
|
2413
|
+
def dataclass_replace_1d(instance: LocalStencil1D, **updates: object) -> LocalStencil1D:
|
|
2414
|
+
"""Small ``dataclasses.replace`` helper that keeps this module self-contained."""
|
|
2415
|
+
|
|
2416
|
+
return LocalStencil1D(
|
|
2417
|
+
center=updates.get("center", instance.center),
|
|
2418
|
+
minus=updates.get("minus", instance.minus),
|
|
2419
|
+
plus=updates.get("plus", instance.plus),
|
|
2420
|
+
dx_min=updates.get("dx_min", instance.dx_min),
|
|
2421
|
+
dx_plus=updates.get("dx_plus", instance.dx_plus),
|
|
2422
|
+
derivative_minus_weight=updates.get(
|
|
2423
|
+
"derivative_minus_weight",
|
|
2424
|
+
instance.derivative_minus_weight,
|
|
2425
|
+
),
|
|
2426
|
+
derivative_center_weight=updates.get(
|
|
2427
|
+
"derivative_center_weight",
|
|
2428
|
+
instance.derivative_center_weight,
|
|
2429
|
+
),
|
|
2430
|
+
derivative_plus_weight=updates.get(
|
|
2431
|
+
"derivative_plus_weight",
|
|
2432
|
+
instance.derivative_plus_weight,
|
|
2433
|
+
),
|
|
2434
|
+
)
|
|
2435
|
+
|
|
2436
|
+
|
|
2437
|
+
def dataclass_replace_3d(instance: LocalStencil3D, **updates: object) -> LocalStencil3D:
|
|
2438
|
+
"""Small ``dataclasses.replace`` helper that keeps this module self-contained."""
|
|
2439
|
+
|
|
2440
|
+
return LocalStencil3D(
|
|
2441
|
+
x=updates.get("x", instance.x),
|
|
2442
|
+
y=updates.get("y", instance.y),
|
|
2443
|
+
z=updates.get("z", instance.z),
|
|
2444
|
+
)
|
|
2445
|
+
|
|
2446
|
+
|
|
2447
|
+
def dataclass_replace_conservative(
|
|
2448
|
+
instance: ConservativeStencil3D, **updates: object
|
|
2449
|
+
) -> ConservativeStencil3D:
|
|
2450
|
+
"""Small ``dataclasses.replace`` helper for conservative stencil payloads."""
|
|
2451
|
+
|
|
2452
|
+
return ConservativeStencil3D(
|
|
2453
|
+
x=updates.get("x", instance.x),
|
|
2454
|
+
y=updates.get("y", instance.y),
|
|
2455
|
+
z=updates.get("z", instance.z),
|
|
2456
|
+
face_grad=updates.get("face_grad", instance.face_grad),
|
|
2457
|
+
)
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
__all__ = [
|
|
2461
|
+
"BC_DIRICHLET",
|
|
2462
|
+
"BC_NEUMANN",
|
|
2463
|
+
"BC_NONE",
|
|
2464
|
+
"BC_NORMALFLUX",
|
|
2465
|
+
"BC_NOFLUX",
|
|
2466
|
+
"BoundaryConditionBuilder",
|
|
2467
|
+
"BoundaryFaceBC3D",
|
|
2468
|
+
"CellVolumeGeometry3D",
|
|
2469
|
+
"CoordinateFaceValueReconstructor3D",
|
|
2470
|
+
"CoordinateNormalDerivativeConstructor3D",
|
|
2471
|
+
"FaceGradientStencil3D",
|
|
2472
|
+
"ConservativeStencil3D",
|
|
2473
|
+
"CutWallBC3D",
|
|
2474
|
+
"CutWallGeometry3D",
|
|
2475
|
+
"CutWallNormalDerivativeConstructor3D",
|
|
2476
|
+
"CutWallValueReconstructor3D",
|
|
2477
|
+
"FaceFluxStencil3D",
|
|
2478
|
+
"FourFieldBoundaryConditions",
|
|
2479
|
+
"LocalBoundaryConditionBuilder",
|
|
2480
|
+
"LocalBoundaryData3D",
|
|
2481
|
+
"LocalBoundaryFaceBC3D",
|
|
2482
|
+
"LocalCoordinateFaceValueReconstructor3D",
|
|
2483
|
+
"LocalCoordinateNormalDerivativeConstructor3D",
|
|
2484
|
+
"LocalCoordinateSideValues1D",
|
|
2485
|
+
"LocalCoordinateSideValues3D",
|
|
2486
|
+
"LocalControlVolumeFluxStencil3D",
|
|
2487
|
+
"LocalCutWallBC3D",
|
|
2488
|
+
"LocalCutWallGeometry3D",
|
|
2489
|
+
"LocalCutWallNormalDerivativeConstructor3D",
|
|
2490
|
+
"LocalCutWallValueReconstructor3D",
|
|
2491
|
+
"LocalStencil1D",
|
|
2492
|
+
"LocalStencil3D",
|
|
2493
|
+
"RegularFaceGeometry3D",
|
|
2494
|
+
]
|