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
drbx/native/fci_halo.py
ADDED
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
"""Halo exchange for shard-local 3D fields.
|
|
2
|
+
|
|
3
|
+
This stage only fills halos at interfaces between logical shards. It does not
|
|
4
|
+
decide how a global physical boundary, topology boundary, or cut wall should
|
|
5
|
+
be represented. Those operations belong to later field-preparer stages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
import jax
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
from jax import lax
|
|
16
|
+
|
|
17
|
+
from ..geometry.fci_geometry import (
|
|
18
|
+
FCI_DEP_CUT_WALL,
|
|
19
|
+
FCI_DEP_FIELD_INTERIOR,
|
|
20
|
+
FCI_DEP_INVALID,
|
|
21
|
+
FCI_DEP_PHYSICAL_BOUNDARY,
|
|
22
|
+
SIDE_AXIS_REGULAR,
|
|
23
|
+
SIDE_SIMPLE_PERIODIC,
|
|
24
|
+
LocalDomain3D,
|
|
25
|
+
LocalFciDirectionMap,
|
|
26
|
+
LocalFciGeometry3D,
|
|
27
|
+
StencilBuilderContext,
|
|
28
|
+
_DataclassPyTreeMixin,
|
|
29
|
+
)
|
|
30
|
+
from .fci_boundaries import (
|
|
31
|
+
BC_DIRICHLET,
|
|
32
|
+
BC_NEUMANN,
|
|
33
|
+
LocalBoundaryFaceBC3D,
|
|
34
|
+
LocalBoundaryConditionBuilder,
|
|
35
|
+
LocalBoundaryData3D,
|
|
36
|
+
LocalCutWallBC3D,
|
|
37
|
+
LocalCutWallGeometry3D,
|
|
38
|
+
LocalCutWallValueReconstructor3D,
|
|
39
|
+
)
|
|
40
|
+
from .fci_model import (
|
|
41
|
+
FciFieldBundle,
|
|
42
|
+
FciModelState,
|
|
43
|
+
FciModelStateT,
|
|
44
|
+
assert_matching_field_names,
|
|
45
|
+
inject_owned_state_to_halo,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_pytree_base = jax.tree_util.register_pytree_node_class
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_halo_spatial_prefix(
|
|
53
|
+
field_halo: jnp.ndarray,
|
|
54
|
+
domain: LocalDomain3D,
|
|
55
|
+
*,
|
|
56
|
+
name: str = "field_halo",
|
|
57
|
+
) -> jnp.ndarray:
|
|
58
|
+
"""Validate the spatial part of a scalar or trailing-axis halo field."""
|
|
59
|
+
|
|
60
|
+
field_halo = jnp.asarray(field_halo)
|
|
61
|
+
expected_shape = tuple(int(value) for value in domain.layout.cell_halo_shape)
|
|
62
|
+
if field_halo.ndim < 3 or tuple(field_halo.shape[:3]) != expected_shape:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"{name} leading shape must match domain.layout.cell_halo_shape; "
|
|
65
|
+
f"got {field_halo.shape}, expected prefix {expected_shape}"
|
|
66
|
+
)
|
|
67
|
+
return field_halo
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _trailing_slices(ndim: int) -> tuple[slice, ...]:
|
|
71
|
+
"""Return full slices for every non-spatial axis of a field."""
|
|
72
|
+
|
|
73
|
+
if ndim < 3:
|
|
74
|
+
raise ValueError(f"a halo field must have at least three axes, got ndim={ndim}")
|
|
75
|
+
return (slice(None),) * (ndim - 3)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@_pytree_base
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class HaloExchange3D(_DataclassPyTreeMixin):
|
|
81
|
+
"""Exchange face halos using JAX SPMD collectives.
|
|
82
|
+
|
|
83
|
+
This backend is intended to run inside ``shard_map``/``pmap``-style SPMD
|
|
84
|
+
code where each configured ``LocalDomain3D.mesh_axis_names`` entry is a
|
|
85
|
+
valid collective axis name. It exchanges only the six face slabs of a
|
|
86
|
+
cell-centered field. Trailing component axes are carried unchanged, so a
|
|
87
|
+
vector field can be exchanged in one call:
|
|
88
|
+
halo edges and corners are deliberately left for a later topology or
|
|
89
|
+
ghost-cell stage.
|
|
90
|
+
|
|
91
|
+
Global side kinds in ``ShardSpec3D`` control whether a wrapped collective
|
|
92
|
+
is allowed. Internal shard interfaces are always exchanged; global sides
|
|
93
|
+
are exchanged only when marked ``SIDE_SIMPLE_PERIODIC``. Physical,
|
|
94
|
+
axis-regular, topology-mapped, and unused sides are left unchanged for
|
|
95
|
+
their owning later stage. With one shard on an axis, no collective is
|
|
96
|
+
issued; topology and physical stages own those sides.
|
|
97
|
+
|
|
98
|
+
For halo width ``h``, one direct neighbor exchange is sufficient when the
|
|
99
|
+
owned extent on every exchanged axis is at least ``h``. Wider halos that
|
|
100
|
+
span multiple neighboring shards require a different communication plan
|
|
101
|
+
and are rejected here rather than being silently filled incorrectly.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
exchange_axes: tuple[bool, bool, bool] = (True, True, True)
|
|
105
|
+
|
|
106
|
+
def __post_init__(self) -> None:
|
|
107
|
+
exchange_axes = tuple(bool(value) for value in self.exchange_axes)
|
|
108
|
+
|
|
109
|
+
if len(exchange_axes) != 3:
|
|
110
|
+
raise ValueError("HaloExchange3D.exchange_axes must have length 3")
|
|
111
|
+
object.__setattr__(self, "exchange_axes", exchange_axes)
|
|
112
|
+
|
|
113
|
+
def __call__(
|
|
114
|
+
self,
|
|
115
|
+
field_halo: jnp.ndarray,
|
|
116
|
+
domain: LocalDomain3D,
|
|
117
|
+
) -> jnp.ndarray:
|
|
118
|
+
"""Fill only regular-neighbor and decomposed simple-periodic face halos."""
|
|
119
|
+
|
|
120
|
+
if not isinstance(domain, LocalDomain3D):
|
|
121
|
+
raise TypeError("HaloExchange3D.domain must be a LocalDomain3D instance")
|
|
122
|
+
|
|
123
|
+
shard_counts = tuple(int(value) for value in domain.shard_spec.shard_counts)
|
|
124
|
+
if len(domain.mesh_axis_names) != 3:
|
|
125
|
+
raise ValueError("domain.mesh_axis_names must have length 3")
|
|
126
|
+
for axis, (enabled, count, name) in enumerate(
|
|
127
|
+
zip(self.exchange_axes, shard_counts, domain.mesh_axis_names)
|
|
128
|
+
):
|
|
129
|
+
if enabled and count > 1 and not name:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"HaloExchange3D requires a mesh axis name in "
|
|
132
|
+
f"domain.mesh_axis_names[{axis}] for decomposed exchange"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
field_halo = _validate_halo_spatial_prefix(field_halo, domain)
|
|
136
|
+
|
|
137
|
+
h = int(domain.layout.halo_width)
|
|
138
|
+
if h == 0:
|
|
139
|
+
return field_halo
|
|
140
|
+
|
|
141
|
+
for axis, (enabled, shard_count) in enumerate(zip(self.exchange_axes, shard_counts)):
|
|
142
|
+
if enabled and shard_count > 1 and h > domain.owned_shape[axis]:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"HaloExchange3D requires halo_width no larger than the owned "
|
|
145
|
+
"extent on each exchanged axis; "
|
|
146
|
+
f"axis={axis}, halo_width={h}, "
|
|
147
|
+
f"owned_extent={domain.owned_shape[axis]}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
result = field_halo
|
|
151
|
+
for axis in range(3):
|
|
152
|
+
result = self._exchange_axis(result, domain, axis=axis)
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def _exchange_axis(
|
|
156
|
+
self,
|
|
157
|
+
field_halo: jnp.ndarray,
|
|
158
|
+
domain: LocalDomain3D,
|
|
159
|
+
*,
|
|
160
|
+
axis: int,
|
|
161
|
+
) -> jnp.ndarray:
|
|
162
|
+
"""Exchange one coordinate-direction face slab."""
|
|
163
|
+
|
|
164
|
+
axis = int(axis)
|
|
165
|
+
if axis < 0 or axis > 2:
|
|
166
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
167
|
+
|
|
168
|
+
if not self.exchange_axes[axis]:
|
|
169
|
+
return field_halo
|
|
170
|
+
|
|
171
|
+
axis_name = domain.mesh_axis_names[axis]
|
|
172
|
+
shard_count = int(domain.shard_spec.shard_counts[axis])
|
|
173
|
+
|
|
174
|
+
# A one-shard axis has no internal interface. Its periodic or physical
|
|
175
|
+
# outer halos are filled by later stages.
|
|
176
|
+
if shard_count <= 1 or axis_name is None:
|
|
177
|
+
return field_halo
|
|
178
|
+
|
|
179
|
+
layout = domain.layout
|
|
180
|
+
h = int(layout.halo_width)
|
|
181
|
+
nx, ny, nz = layout.owned_shape
|
|
182
|
+
|
|
183
|
+
i = slice(h, h + nx)
|
|
184
|
+
j = slice(h, h + ny)
|
|
185
|
+
k = slice(h, h + nz)
|
|
186
|
+
trailing = _trailing_slices(field_halo.ndim)
|
|
187
|
+
|
|
188
|
+
if axis == 0:
|
|
189
|
+
lower_owned_slab = field_halo[(slice(h, h + h), j, k) + trailing]
|
|
190
|
+
upper_owned_slab = field_halo[(slice(h + nx - h, h + nx), j, k) + trailing]
|
|
191
|
+
lower_halo_index = (slice(0, h), j, k) + trailing
|
|
192
|
+
upper_halo_index = (slice(h + nx, h + nx + h), j, k) + trailing
|
|
193
|
+
elif axis == 1:
|
|
194
|
+
lower_owned_slab = field_halo[(i, slice(h, h + h), k) + trailing]
|
|
195
|
+
upper_owned_slab = field_halo[(i, slice(h + ny - h, h + ny), k) + trailing]
|
|
196
|
+
lower_halo_index = (i, slice(0, h), k) + trailing
|
|
197
|
+
upper_halo_index = (i, slice(h + ny, h + ny + h), k) + trailing
|
|
198
|
+
else:
|
|
199
|
+
lower_owned_slab = field_halo[(i, j, slice(h, h + h)) + trailing]
|
|
200
|
+
upper_owned_slab = field_halo[(i, j, slice(h + nz - h, h + nz)) + trailing]
|
|
201
|
+
lower_halo_index = (i, j, slice(0, h)) + trailing
|
|
202
|
+
upper_halo_index = (i, j, slice(h + nz, h + nz + h)) + trailing
|
|
203
|
+
|
|
204
|
+
shard_id = lax.axis_index(axis_name)
|
|
205
|
+
# The domain metadata may be closed over by an SPMD function and can
|
|
206
|
+
# therefore describe one representative shard. Use the runtime
|
|
207
|
+
# collective index to decide whether this rank is on a global side;
|
|
208
|
+
# the side-kind metadata describes those global sides.
|
|
209
|
+
lower_global_allowed = (
|
|
210
|
+
domain.shard_spec.lower_side_kind(axis) == SIDE_SIMPLE_PERIODIC
|
|
211
|
+
)
|
|
212
|
+
upper_global_allowed = (
|
|
213
|
+
domain.shard_spec.upper_side_kind(axis) == SIDE_SIMPLE_PERIODIC
|
|
214
|
+
)
|
|
215
|
+
fill_lower_halo = jnp.where(
|
|
216
|
+
shard_id > 0,
|
|
217
|
+
True,
|
|
218
|
+
lower_global_allowed,
|
|
219
|
+
)
|
|
220
|
+
fill_upper_halo = jnp.where(
|
|
221
|
+
shard_id < shard_count - 1,
|
|
222
|
+
True,
|
|
223
|
+
upper_global_allowed,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# ppermute maps source shard -> destination shard. Sending the upper
|
|
227
|
+
# slab toward increasing shard ids supplies a lower halo; sending the
|
|
228
|
+
# lower slab toward decreasing shard ids supplies an upper halo.
|
|
229
|
+
recv_from_minus = lax.ppermute(
|
|
230
|
+
upper_owned_slab,
|
|
231
|
+
axis_name=axis_name,
|
|
232
|
+
perm=[(source, (source + 1) % shard_count) for source in range(shard_count)],
|
|
233
|
+
)
|
|
234
|
+
recv_from_plus = lax.ppermute(
|
|
235
|
+
lower_owned_slab,
|
|
236
|
+
axis_name=axis_name,
|
|
237
|
+
perm=[(source, (source - 1) % shard_count) for source in range(shard_count)],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
old_lower_halo = field_halo[lower_halo_index]
|
|
241
|
+
old_upper_halo = field_halo[upper_halo_index]
|
|
242
|
+
new_lower_halo = jnp.where(
|
|
243
|
+
fill_lower_halo, recv_from_minus, old_lower_halo
|
|
244
|
+
)
|
|
245
|
+
new_upper_halo = jnp.where(
|
|
246
|
+
fill_upper_halo, recv_from_plus, old_upper_halo
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
field_halo = field_halo.at[lower_halo_index].set(new_lower_halo)
|
|
250
|
+
field_halo = field_halo.at[upper_halo_index].set(new_upper_halo)
|
|
251
|
+
return field_halo
|
|
252
|
+
|
|
253
|
+
def tree_flatten(self):
|
|
254
|
+
return (), (self.exchange_axes,)
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def tree_unflatten(cls, aux_data, children):
|
|
258
|
+
del children
|
|
259
|
+
(exchange_axes,) = aux_data
|
|
260
|
+
return cls(exchange_axes=exchange_axes)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class FciCutWallValueEvaluator(Protocol):
|
|
264
|
+
"""Protocol for owner-local FCI cut-wall value evaluation."""
|
|
265
|
+
|
|
266
|
+
def __call__(
|
|
267
|
+
self,
|
|
268
|
+
*,
|
|
269
|
+
field_halo: jnp.ndarray,
|
|
270
|
+
cut_wall_geometry: LocalCutWallGeometry3D | None,
|
|
271
|
+
cut_wall_bc: LocalCutWallBC3D | None,
|
|
272
|
+
context: StencilBuilderContext,
|
|
273
|
+
value_slot: jnp.ndarray,
|
|
274
|
+
active: jnp.ndarray,
|
|
275
|
+
) -> jnp.ndarray:
|
|
276
|
+
"""Return one scalar cut-wall value per requested slot."""
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@_pytree_base
|
|
281
|
+
@dataclass(frozen=True)
|
|
282
|
+
class LocalFciCutWallValueEvaluator(_DataclassPyTreeMixin):
|
|
283
|
+
"""Evaluate owner-local FCI cut-wall endpoint values.
|
|
284
|
+
|
|
285
|
+
``value_slot`` indexes padded owner-local cut-wall metadata. The concrete
|
|
286
|
+
wall-value reconstruction is delegated to
|
|
287
|
+
``context.cut_wall_value_reconstructor`` so the exchange uses the same
|
|
288
|
+
prepared-halo stencil data as the local FCI stencil builder. Dirichlet rows
|
|
289
|
+
override that reconstructed value with their prescribed wall value;
|
|
290
|
+
Neumann and flux-style rows use the reconstructed value directly.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def __call__(
|
|
294
|
+
self,
|
|
295
|
+
*,
|
|
296
|
+
field_halo: jnp.ndarray,
|
|
297
|
+
cut_wall_geometry: LocalCutWallGeometry3D | None,
|
|
298
|
+
cut_wall_bc: LocalCutWallBC3D | None,
|
|
299
|
+
context: StencilBuilderContext,
|
|
300
|
+
value_slot: jnp.ndarray,
|
|
301
|
+
active: jnp.ndarray,
|
|
302
|
+
) -> jnp.ndarray:
|
|
303
|
+
if not isinstance(context, StencilBuilderContext):
|
|
304
|
+
raise TypeError("context must be a StencilBuilderContext instance")
|
|
305
|
+
|
|
306
|
+
field_halo = jnp.asarray(field_halo)
|
|
307
|
+
if field_halo.ndim != 3:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"LocalFciCutWallValueEvaluator currently supports scalar "
|
|
310
|
+
f"halo fields only; got shape {field_halo.shape}"
|
|
311
|
+
)
|
|
312
|
+
if field_halo.shape != context.layout.cell_halo_shape:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
"field_halo must match context.layout.cell_halo_shape; "
|
|
315
|
+
f"got {field_halo.shape}, expected {context.layout.cell_halo_shape}"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
value_slot = jnp.asarray(value_slot, dtype=jnp.int32)
|
|
319
|
+
active = jnp.asarray(active, dtype=bool)
|
|
320
|
+
if active.shape != value_slot.shape:
|
|
321
|
+
raise ValueError(
|
|
322
|
+
"active must have the same shape as value_slot; "
|
|
323
|
+
f"got active={active.shape}, value_slot={value_slot.shape}"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if cut_wall_geometry is None or cut_wall_bc is None:
|
|
327
|
+
return jnp.zeros(value_slot.shape, dtype=field_halo.dtype)
|
|
328
|
+
if not isinstance(cut_wall_geometry, LocalCutWallGeometry3D):
|
|
329
|
+
raise TypeError(
|
|
330
|
+
"cut_wall_geometry must be a LocalCutWallGeometry3D or None"
|
|
331
|
+
)
|
|
332
|
+
if not isinstance(cut_wall_bc, LocalCutWallBC3D):
|
|
333
|
+
raise TypeError("cut_wall_bc must be a LocalCutWallBC3D or None")
|
|
334
|
+
if cut_wall_geometry.max_wall_faces != cut_wall_bc.max_wall_faces:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
"cut_wall_geometry and cut_wall_bc must have the same "
|
|
337
|
+
"max_wall_faces"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
max_wall_faces = int(cut_wall_geometry.max_wall_faces)
|
|
341
|
+
if max_wall_faces == 0:
|
|
342
|
+
return jnp.zeros(value_slot.shape, dtype=field_halo.dtype)
|
|
343
|
+
value_reconstructor = context.cut_wall_value_reconstructor
|
|
344
|
+
if value_reconstructor is None:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
"context.cut_wall_value_reconstructor is required for "
|
|
347
|
+
"nonempty FCI cut-wall value evaluation"
|
|
348
|
+
)
|
|
349
|
+
if not isinstance(value_reconstructor, LocalCutWallValueReconstructor3D):
|
|
350
|
+
raise TypeError(
|
|
351
|
+
"context.cut_wall_value_reconstructor must be a "
|
|
352
|
+
"LocalCutWallValueReconstructor3D"
|
|
353
|
+
)
|
|
354
|
+
if value_reconstructor.max_wall_faces != max_wall_faces:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
"cut_wall_value_reconstructor.max_wall_faces must match "
|
|
357
|
+
"cut_wall_geometry.max_wall_faces"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
safe_slot = jnp.clip(value_slot, 0, max_wall_faces - 1)
|
|
361
|
+
slot_active = (
|
|
362
|
+
jnp.asarray(cut_wall_geometry.active, dtype=bool)[safe_slot]
|
|
363
|
+
& jnp.asarray(cut_wall_bc.active, dtype=bool)[safe_slot]
|
|
364
|
+
& jnp.asarray(value_reconstructor.active, dtype=bool)[safe_slot]
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
wall_values = jnp.asarray(
|
|
368
|
+
value_reconstructor.extrapolate(field_halo),
|
|
369
|
+
dtype=field_halo.dtype,
|
|
370
|
+
)
|
|
371
|
+
cut_wall_kind = jnp.asarray(cut_wall_bc.kind, dtype=jnp.int32)
|
|
372
|
+
cut_wall_value = jnp.asarray(cut_wall_bc.value, dtype=field_halo.dtype)
|
|
373
|
+
wall_values = jnp.where(
|
|
374
|
+
cut_wall_kind == BC_DIRICHLET,
|
|
375
|
+
cut_wall_value,
|
|
376
|
+
wall_values,
|
|
377
|
+
)
|
|
378
|
+
values = wall_values[safe_slot]
|
|
379
|
+
return jnp.where(active & slot_active, values, jnp.zeros_like(values))
|
|
380
|
+
|
|
381
|
+
def tree_flatten(self):
|
|
382
|
+
return (), None
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
386
|
+
del children
|
|
387
|
+
return cls()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@_pytree_base
|
|
391
|
+
@dataclass(frozen=True)
|
|
392
|
+
class RemoteFciDependencyExchange(_DataclassPyTreeMixin):
|
|
393
|
+
"""Populate remote FCI receive values for one trace direction.
|
|
394
|
+
|
|
395
|
+
The exchange consumes the request side of ``LocalFciRemoteDependencyTable``
|
|
396
|
+
and returns a local vector where request row ``q`` is receive slot ``q``.
|
|
397
|
+
It communicates scalar values only; the FCI stencil builder owns endpoint
|
|
398
|
+
assembly from local rows plus these returned remote values.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
cut_wall_evaluator: FciCutWallValueEvaluator = LocalFciCutWallValueEvaluator()
|
|
402
|
+
|
|
403
|
+
def __call__(
|
|
404
|
+
self,
|
|
405
|
+
*,
|
|
406
|
+
field_halo: jnp.ndarray,
|
|
407
|
+
direction: LocalFciDirectionMap,
|
|
408
|
+
context: StencilBuilderContext,
|
|
409
|
+
) -> jnp.ndarray:
|
|
410
|
+
if not isinstance(direction, LocalFciDirectionMap):
|
|
411
|
+
raise TypeError("direction must be a LocalFciDirectionMap instance")
|
|
412
|
+
if not isinstance(context, StencilBuilderContext):
|
|
413
|
+
raise TypeError("context must be a StencilBuilderContext instance")
|
|
414
|
+
if context.domain is None:
|
|
415
|
+
raise ValueError("context.domain is required for remote FCI exchange")
|
|
416
|
+
if context.layout != direction.layout:
|
|
417
|
+
raise ValueError("context and direction must share the same HaloLayout3D")
|
|
418
|
+
|
|
419
|
+
table = direction.remote
|
|
420
|
+
field_halo = jnp.asarray(field_halo)
|
|
421
|
+
if field_halo.ndim != 3:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"RemoteFciDependencyExchange currently supports scalar halo "
|
|
424
|
+
f"fields only; got shape {field_halo.shape}"
|
|
425
|
+
)
|
|
426
|
+
if field_halo.shape != direction.layout.cell_halo_shape:
|
|
427
|
+
raise ValueError(
|
|
428
|
+
"field_halo must match direction.layout.cell_halo_shape; "
|
|
429
|
+
f"got {field_halo.shape}, expected {direction.layout.cell_halo_shape}"
|
|
430
|
+
)
|
|
431
|
+
if table is None:
|
|
432
|
+
return jnp.zeros((0,), dtype=field_halo.dtype)
|
|
433
|
+
|
|
434
|
+
domain = context.domain
|
|
435
|
+
self._validate_mesh_axes(domain)
|
|
436
|
+
|
|
437
|
+
shard_counts = tuple(int(value) for value in domain.shard_spec.shard_counts)
|
|
438
|
+
n_shards = shard_counts[0] * shard_counts[1] * shard_counts[2]
|
|
439
|
+
n_requests = int(table.max_receive_values)
|
|
440
|
+
|
|
441
|
+
shard_x = jnp.asarray(domain.runtime_shard_id(0), dtype=jnp.int32)
|
|
442
|
+
shard_y = jnp.asarray(domain.runtime_shard_id(1), dtype=jnp.int32)
|
|
443
|
+
shard_z = jnp.asarray(domain.runtime_shard_id(2), dtype=jnp.int32)
|
|
444
|
+
my_shard_linear = (
|
|
445
|
+
shard_z * (shard_counts[1] * shard_counts[0])
|
|
446
|
+
+ shard_y * shard_counts[0]
|
|
447
|
+
+ shard_x
|
|
448
|
+
).astype(jnp.int32)
|
|
449
|
+
|
|
450
|
+
request_active = self._all_gather_flat(table.request_active, domain)
|
|
451
|
+
request_kind = self._all_gather_flat(table.request_dependency_kind, domain)
|
|
452
|
+
request_owner_linear = self._all_gather_flat(
|
|
453
|
+
table.request_source_shard_linear,
|
|
454
|
+
domain,
|
|
455
|
+
)
|
|
456
|
+
request_i = self._all_gather_flat(
|
|
457
|
+
table.request_source_owner_local_i,
|
|
458
|
+
domain,
|
|
459
|
+
)
|
|
460
|
+
request_j = self._all_gather_flat(
|
|
461
|
+
table.request_source_owner_local_j,
|
|
462
|
+
domain,
|
|
463
|
+
)
|
|
464
|
+
request_k = self._all_gather_flat(
|
|
465
|
+
table.request_source_owner_local_k,
|
|
466
|
+
domain,
|
|
467
|
+
)
|
|
468
|
+
request_value_slot = self._all_gather_flat(
|
|
469
|
+
table.request_value_slot,
|
|
470
|
+
domain,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
owned_by_me = request_owner_linear == my_shard_linear
|
|
474
|
+
supported_kind = (
|
|
475
|
+
(request_kind == FCI_DEP_FIELD_INTERIOR)
|
|
476
|
+
| (request_kind == FCI_DEP_PHYSICAL_BOUNDARY)
|
|
477
|
+
| (request_kind == FCI_DEP_CUT_WALL)
|
|
478
|
+
)
|
|
479
|
+
valid_request = request_active & owned_by_me & supported_kind
|
|
480
|
+
field_request = valid_request & (
|
|
481
|
+
(request_kind == FCI_DEP_FIELD_INTERIOR)
|
|
482
|
+
| (request_kind == FCI_DEP_PHYSICAL_BOUNDARY)
|
|
483
|
+
)
|
|
484
|
+
cut_wall_request = valid_request & (request_kind == FCI_DEP_CUT_WALL)
|
|
485
|
+
|
|
486
|
+
field_values = self._sample_field_halo(
|
|
487
|
+
field_halo=field_halo,
|
|
488
|
+
source_i=request_i,
|
|
489
|
+
source_j=request_j,
|
|
490
|
+
source_k=request_k,
|
|
491
|
+
)
|
|
492
|
+
cut_wall_values = self.cut_wall_evaluator(
|
|
493
|
+
field_halo=field_halo,
|
|
494
|
+
cut_wall_geometry=context.cut_wall_geometry,
|
|
495
|
+
cut_wall_bc=context.cut_wall_bc,
|
|
496
|
+
context=context,
|
|
497
|
+
value_slot=request_value_slot,
|
|
498
|
+
active=cut_wall_request,
|
|
499
|
+
)
|
|
500
|
+
if cut_wall_values.shape != cut_wall_request.shape:
|
|
501
|
+
raise ValueError(
|
|
502
|
+
"cut_wall_evaluator must return an array with shape "
|
|
503
|
+
f"{cut_wall_request.shape}, got {cut_wall_values.shape}"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
owner_responses = jnp.zeros((n_shards, n_requests), dtype=field_halo.dtype)
|
|
507
|
+
owner_responses = jnp.where(field_request, field_values, owner_responses)
|
|
508
|
+
owner_responses = jnp.where(cut_wall_request, cut_wall_values, owner_responses)
|
|
509
|
+
|
|
510
|
+
responses_by_requester = self._psum_over_mesh_axes(owner_responses, domain)
|
|
511
|
+
remote_values = jnp.take(responses_by_requester, my_shard_linear, axis=0)
|
|
512
|
+
active_remote = table.request_active & (
|
|
513
|
+
table.request_dependency_kind != FCI_DEP_INVALID
|
|
514
|
+
)
|
|
515
|
+
return jnp.where(active_remote, remote_values, jnp.zeros_like(remote_values))
|
|
516
|
+
|
|
517
|
+
@staticmethod
|
|
518
|
+
def _validate_mesh_axes(domain: LocalDomain3D) -> None:
|
|
519
|
+
for axis, (count, name) in enumerate(
|
|
520
|
+
zip(domain.shard_spec.shard_counts, domain.mesh_axis_names)
|
|
521
|
+
):
|
|
522
|
+
if int(count) > 1 and name is None:
|
|
523
|
+
raise ValueError(
|
|
524
|
+
"RemoteFciDependencyExchange requires a mesh axis name in "
|
|
525
|
+
f"domain.mesh_axis_names[{axis}] for decomposed exchange"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
@staticmethod
|
|
529
|
+
def _sample_field_halo(
|
|
530
|
+
*,
|
|
531
|
+
field_halo: jnp.ndarray,
|
|
532
|
+
source_i: jnp.ndarray,
|
|
533
|
+
source_j: jnp.ndarray,
|
|
534
|
+
source_k: jnp.ndarray,
|
|
535
|
+
) -> jnp.ndarray:
|
|
536
|
+
nx, ny, nz = field_halo.shape
|
|
537
|
+
safe_i = jnp.clip(source_i, 0, nx - 1)
|
|
538
|
+
safe_j = jnp.clip(source_j, 0, ny - 1)
|
|
539
|
+
safe_k = jnp.clip(source_k, 0, nz - 1)
|
|
540
|
+
return field_halo[safe_i, safe_j, safe_k]
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _all_gather_flat(value: jnp.ndarray, domain: LocalDomain3D) -> jnp.ndarray:
|
|
544
|
+
gathered = value
|
|
545
|
+
for shard_count, axis_name in zip(
|
|
546
|
+
domain.shard_spec.shard_counts,
|
|
547
|
+
domain.mesh_axis_names,
|
|
548
|
+
):
|
|
549
|
+
if int(shard_count) > 1 and axis_name is not None:
|
|
550
|
+
gathered = lax.all_gather(
|
|
551
|
+
gathered,
|
|
552
|
+
axis_name=axis_name,
|
|
553
|
+
axis=0,
|
|
554
|
+
tiled=False,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
shard_counts = tuple(int(v) for v in domain.shard_spec.shard_counts)
|
|
558
|
+
n_shards = shard_counts[0] * shard_counts[1] * shard_counts[2]
|
|
559
|
+
return gathered.reshape((n_shards,) + value.shape)
|
|
560
|
+
|
|
561
|
+
@staticmethod
|
|
562
|
+
def _psum_over_mesh_axes(value: jnp.ndarray, domain: LocalDomain3D) -> jnp.ndarray:
|
|
563
|
+
result = value
|
|
564
|
+
for shard_count, axis_name in zip(
|
|
565
|
+
domain.shard_spec.shard_counts,
|
|
566
|
+
domain.mesh_axis_names,
|
|
567
|
+
):
|
|
568
|
+
if int(shard_count) > 1 and axis_name is not None:
|
|
569
|
+
result = lax.psum(result, axis_name=axis_name)
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
def tree_flatten(self):
|
|
573
|
+
return (), self.cut_wall_evaluator
|
|
574
|
+
|
|
575
|
+
@classmethod
|
|
576
|
+
def tree_unflatten(cls, aux_data, children):
|
|
577
|
+
del children
|
|
578
|
+
return cls(cut_wall_evaluator=aux_data)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@_pytree_base
|
|
582
|
+
@dataclass(frozen=True)
|
|
583
|
+
class TopologyHaloFiller3D(_DataclassPyTreeMixin):
|
|
584
|
+
"""Apply ordered topology rules to a halo field.
|
|
585
|
+
|
|
586
|
+
The first three axes are spatial; any trailing component axes are carried
|
|
587
|
+
through the rule pipeline unchanged.
|
|
588
|
+
|
|
589
|
+
Rules have the callable interface ``rule(field_halo, domain)``. The runner
|
|
590
|
+
itself performs no communication directly; an individual rule may be
|
|
591
|
+
local-only or may perform distributed collectives such as ``ppermute`` or
|
|
592
|
+
``all_to_all``. Each rule owns its side-kind checks, communication plan,
|
|
593
|
+
index remaps, and write masks.
|
|
594
|
+
|
|
595
|
+
The runner never fills physical ghost cells and does not promise
|
|
596
|
+
edge/corner values unless an individual rule explicitly provides them.
|
|
597
|
+
Rules are applied in order, so later rules may intentionally overwrite
|
|
598
|
+
values written by earlier rules.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
rules: tuple[object, ...]
|
|
602
|
+
error_if_no_rules: bool = False
|
|
603
|
+
|
|
604
|
+
def __post_init__(self) -> None:
|
|
605
|
+
rules = tuple(self.rules)
|
|
606
|
+
if self.error_if_no_rules and not rules:
|
|
607
|
+
raise ValueError(
|
|
608
|
+
"TopologyHaloFiller3D.rules must be nonempty when "
|
|
609
|
+
"error_if_no_rules=True"
|
|
610
|
+
)
|
|
611
|
+
for index, rule in enumerate(rules):
|
|
612
|
+
if not callable(rule):
|
|
613
|
+
raise TypeError(
|
|
614
|
+
f"TopologyHaloFiller3D.rules[{index}] must be callable"
|
|
615
|
+
)
|
|
616
|
+
object.__setattr__(self, "rules", rules)
|
|
617
|
+
object.__setattr__(self, "error_if_no_rules", bool(self.error_if_no_rules))
|
|
618
|
+
|
|
619
|
+
def __call__(self, field_halo: jnp.ndarray, domain: LocalDomain3D) -> jnp.ndarray:
|
|
620
|
+
if not isinstance(domain, LocalDomain3D):
|
|
621
|
+
raise TypeError("TopologyHaloFiller3D.domain must be a LocalDomain3D instance")
|
|
622
|
+
field_halo = _validate_halo_spatial_prefix(field_halo, domain)
|
|
623
|
+
expected_shape = tuple(int(value) for value in domain.layout.cell_halo_shape)
|
|
624
|
+
expected_trailing_shape = tuple(field_halo.shape[3:])
|
|
625
|
+
result = field_halo
|
|
626
|
+
for rule in self.rules:
|
|
627
|
+
result = rule(result, domain)
|
|
628
|
+
if (
|
|
629
|
+
result.ndim < 3
|
|
630
|
+
or tuple(result.shape[:3]) != expected_shape
|
|
631
|
+
or tuple(result.shape[3:]) != expected_trailing_shape
|
|
632
|
+
):
|
|
633
|
+
raise ValueError(
|
|
634
|
+
"Topology rule returned an invalid field shape; expected "
|
|
635
|
+
f"prefix {expected_shape} with trailing shape "
|
|
636
|
+
f"{expected_trailing_shape}, got {result.shape}"
|
|
637
|
+
)
|
|
638
|
+
return result
|
|
639
|
+
|
|
640
|
+
def tree_flatten(self):
|
|
641
|
+
return (self.rules,), self.error_if_no_rules
|
|
642
|
+
|
|
643
|
+
@classmethod
|
|
644
|
+
def tree_unflatten(cls, aux_data, children):
|
|
645
|
+
(rules,) = children
|
|
646
|
+
return cls(rules=rules, error_if_no_rules=aux_data)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@_pytree_base
|
|
650
|
+
@dataclass(frozen=True)
|
|
651
|
+
class LocalPeriodicTopologyRule3D(_DataclassPyTreeMixin):
|
|
652
|
+
"""Fill undecomposed ``SIDE_SIMPLE_PERIODIC`` face halos locally."""
|
|
653
|
+
|
|
654
|
+
fill_axes: tuple[bool, bool, bool] = (True, True, True)
|
|
655
|
+
|
|
656
|
+
def __post_init__(self) -> None:
|
|
657
|
+
fill_axes = tuple(bool(value) for value in self.fill_axes)
|
|
658
|
+
if len(fill_axes) != 3:
|
|
659
|
+
raise ValueError(
|
|
660
|
+
"LocalPeriodicTopologyRule3D.fill_axes must have length 3"
|
|
661
|
+
)
|
|
662
|
+
object.__setattr__(self, "fill_axes", fill_axes)
|
|
663
|
+
|
|
664
|
+
def __call__(self, field_halo: jnp.ndarray, domain: LocalDomain3D) -> jnp.ndarray:
|
|
665
|
+
field_halo = _validate_halo_spatial_prefix(field_halo, domain)
|
|
666
|
+
result = field_halo
|
|
667
|
+
for axis in range(3):
|
|
668
|
+
result = self._fill_axis(result, domain, axis=axis)
|
|
669
|
+
return result
|
|
670
|
+
|
|
671
|
+
def _fill_axis(self, field_halo, domain, *, axis: int):
|
|
672
|
+
if not self.fill_axes[axis]:
|
|
673
|
+
return field_halo
|
|
674
|
+
spec = domain.shard_spec
|
|
675
|
+
if spec.shard_counts[axis] > 1:
|
|
676
|
+
return field_halo
|
|
677
|
+
|
|
678
|
+
lower = domain.runtime_touches_lower(axis) & (
|
|
679
|
+
spec.lower_side_kind(axis) == SIDE_SIMPLE_PERIODIC
|
|
680
|
+
)
|
|
681
|
+
upper = domain.runtime_touches_upper(axis) & (
|
|
682
|
+
spec.upper_side_kind(axis) == SIDE_SIMPLE_PERIODIC
|
|
683
|
+
)
|
|
684
|
+
if domain.layout.halo_width == 0:
|
|
685
|
+
return field_halo
|
|
686
|
+
|
|
687
|
+
h = domain.layout.halo_width
|
|
688
|
+
ext = domain.layout.owned_shape
|
|
689
|
+
owned = tuple(slice(h, h + size) for size in ext)
|
|
690
|
+
trailing = _trailing_slices(field_halo.ndim)
|
|
691
|
+
result = field_halo
|
|
692
|
+
if axis == 0:
|
|
693
|
+
lower_index = (slice(0, h), owned[1], owned[2]) + trailing
|
|
694
|
+
upper_index = (slice(h + ext[0], h + ext[0] + h), owned[1], owned[2]) + trailing
|
|
695
|
+
old_lower = result[lower_index]
|
|
696
|
+
old_upper = result[upper_index]
|
|
697
|
+
lower_value = result[(slice(h + ext[0] - h, h + ext[0]), owned[1], owned[2]) + trailing]
|
|
698
|
+
upper_value = result[(slice(h, h + h), owned[1], owned[2]) + trailing]
|
|
699
|
+
result = result.at[lower_index].set(jnp.where(lower, lower_value, old_lower))
|
|
700
|
+
result = result.at[upper_index].set(jnp.where(upper, upper_value, old_upper))
|
|
701
|
+
elif axis == 1:
|
|
702
|
+
lower_index = (owned[0], slice(0, h), owned[2]) + trailing
|
|
703
|
+
upper_index = (owned[0], slice(h + ext[1], h + ext[1] + h), owned[2]) + trailing
|
|
704
|
+
old_lower = result[lower_index]
|
|
705
|
+
old_upper = result[upper_index]
|
|
706
|
+
lower_value = result[(owned[0], slice(h + ext[1] - h, h + ext[1]), owned[2]) + trailing]
|
|
707
|
+
upper_value = result[(owned[0], slice(h, h + h), owned[2]) + trailing]
|
|
708
|
+
result = result.at[lower_index].set(jnp.where(lower, lower_value, old_lower))
|
|
709
|
+
result = result.at[upper_index].set(jnp.where(upper, upper_value, old_upper))
|
|
710
|
+
elif axis == 2:
|
|
711
|
+
lower_index = (owned[0], owned[1], slice(0, h)) + trailing
|
|
712
|
+
upper_index = (owned[0], owned[1], slice(h + ext[2], h + ext[2] + h)) + trailing
|
|
713
|
+
old_lower = result[lower_index]
|
|
714
|
+
old_upper = result[upper_index]
|
|
715
|
+
lower_value = result[(owned[0], owned[1], slice(h + ext[2] - h, h + ext[2])) + trailing]
|
|
716
|
+
upper_value = result[(owned[0], owned[1], slice(h, h + h)) + trailing]
|
|
717
|
+
result = result.at[lower_index].set(jnp.where(lower, lower_value, old_lower))
|
|
718
|
+
result = result.at[upper_index].set(jnp.where(upper, upper_value, old_upper))
|
|
719
|
+
else:
|
|
720
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
721
|
+
return result
|
|
722
|
+
|
|
723
|
+
def tree_flatten(self):
|
|
724
|
+
return (), self.fill_axes
|
|
725
|
+
|
|
726
|
+
@classmethod
|
|
727
|
+
def tree_unflatten(cls, aux_data, children):
|
|
728
|
+
del children
|
|
729
|
+
return cls(fill_axes=aux_data)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@_pytree_base
|
|
733
|
+
@dataclass(frozen=True)
|
|
734
|
+
class PolarAxisRegularScalarRule3D(_DataclassPyTreeMixin):
|
|
735
|
+
"""Fill scalar polar-axis regularity halos with optional angle sharding."""
|
|
736
|
+
|
|
737
|
+
angle_axis_name: str | None
|
|
738
|
+
radial_axis: int = 0
|
|
739
|
+
angle_axis: int = 1
|
|
740
|
+
passive_axis: int = 2
|
|
741
|
+
fill_lower: bool = True
|
|
742
|
+
fill_upper: bool = False
|
|
743
|
+
require_even_global_angle: bool = True
|
|
744
|
+
|
|
745
|
+
def __post_init__(self) -> None:
|
|
746
|
+
axes = (int(self.radial_axis), int(self.angle_axis), int(self.passive_axis))
|
|
747
|
+
if axes != (0, 1, 2):
|
|
748
|
+
raise NotImplementedError(
|
|
749
|
+
"PolarAxisRegularScalarRule3D currently supports only "
|
|
750
|
+
"(radial_axis, angle_axis, passive_axis) = (0, 1, 2)"
|
|
751
|
+
)
|
|
752
|
+
if self.angle_axis_name is not None and not isinstance(self.angle_axis_name, str):
|
|
753
|
+
raise TypeError(
|
|
754
|
+
"angle_axis_name must be a string or None, got "
|
|
755
|
+
f"{self.angle_axis_name!r}"
|
|
756
|
+
)
|
|
757
|
+
object.__setattr__(self, "radial_axis", axes[0])
|
|
758
|
+
object.__setattr__(self, "angle_axis", axes[1])
|
|
759
|
+
object.__setattr__(self, "passive_axis", axes[2])
|
|
760
|
+
object.__setattr__(self, "fill_lower", bool(self.fill_lower))
|
|
761
|
+
object.__setattr__(self, "fill_upper", bool(self.fill_upper))
|
|
762
|
+
object.__setattr__(
|
|
763
|
+
self,
|
|
764
|
+
"require_even_global_angle",
|
|
765
|
+
bool(self.require_even_global_angle),
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
def __call__(self, field_halo: jnp.ndarray, domain: LocalDomain3D) -> jnp.ndarray:
|
|
769
|
+
if not isinstance(domain, LocalDomain3D):
|
|
770
|
+
raise TypeError(
|
|
771
|
+
"PolarAxisRegularScalarRule3D.domain must be a LocalDomain3D instance"
|
|
772
|
+
)
|
|
773
|
+
field_halo = jnp.asarray(field_halo)
|
|
774
|
+
expected_shape = domain.layout.cell_halo_shape
|
|
775
|
+
if field_halo.shape != expected_shape:
|
|
776
|
+
raise ValueError(
|
|
777
|
+
f"field_halo must have shape {expected_shape}, got {field_halo.shape}"
|
|
778
|
+
)
|
|
779
|
+
result = field_halo
|
|
780
|
+
# Both calls are made on every SPMD shard. Each call performs any
|
|
781
|
+
# required angle collective before applying its runtime radial-side
|
|
782
|
+
# write mask.
|
|
783
|
+
result = self._fill_radial_side(result, domain, side="lower")
|
|
784
|
+
result = self._fill_radial_side(result, domain, side="upper")
|
|
785
|
+
return result
|
|
786
|
+
|
|
787
|
+
def _fill_radial_side(self, field_halo, domain, *, side: str):
|
|
788
|
+
spec = domain.shard_spec
|
|
789
|
+
if side == "lower":
|
|
790
|
+
do_side = bool(self.fill_lower) & domain.runtime_touches_lower(
|
|
791
|
+
self.radial_axis
|
|
792
|
+
) & (
|
|
793
|
+
spec.lower_side_kind(self.radial_axis) == SIDE_AXIS_REGULAR
|
|
794
|
+
)
|
|
795
|
+
elif side == "upper":
|
|
796
|
+
do_side = bool(self.fill_upper) & domain.runtime_touches_upper(
|
|
797
|
+
self.radial_axis
|
|
798
|
+
) & (
|
|
799
|
+
spec.upper_side_kind(self.radial_axis) == SIDE_AXIS_REGULAR
|
|
800
|
+
)
|
|
801
|
+
else:
|
|
802
|
+
raise ValueError(f"side must be 'lower' or 'upper', got {side!r}")
|
|
803
|
+
|
|
804
|
+
angle_axis = self.angle_axis
|
|
805
|
+
if spec.shard_counts[self.radial_axis] > 1 and domain.mesh_axis_names[self.radial_axis] is None:
|
|
806
|
+
raise ValueError(
|
|
807
|
+
"PolarAxisRegularScalarRule3D requires a mesh axis name for a "
|
|
808
|
+
"decomposed radial axis"
|
|
809
|
+
)
|
|
810
|
+
if (
|
|
811
|
+
spec.lower_side_kind(angle_axis) != SIDE_SIMPLE_PERIODIC
|
|
812
|
+
or spec.upper_side_kind(angle_axis) != SIDE_SIMPLE_PERIODIC
|
|
813
|
+
):
|
|
814
|
+
raise ValueError(
|
|
815
|
+
"PolarAxisRegularScalarRule3D requires SIDE_SIMPLE_PERIODIC "
|
|
816
|
+
"on both global angle sides"
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
h = int(domain.layout.halo_width)
|
|
820
|
+
if h == 0:
|
|
821
|
+
return field_halo
|
|
822
|
+
nx, ny, nz = domain.layout.owned_shape
|
|
823
|
+
if h > nx:
|
|
824
|
+
raise ValueError(
|
|
825
|
+
"PolarAxisRegularScalarRule3D requires halo_width <= local "
|
|
826
|
+
f"radial extent; got halo_width={h}, nx={nx}"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
global_angle = int(spec.global_shape[angle_axis])
|
|
830
|
+
local_angle = int(domain.layout.owned_shape[angle_axis])
|
|
831
|
+
angle_count = int(spec.shard_counts[angle_axis])
|
|
832
|
+
if self.require_even_global_angle and global_angle % 2:
|
|
833
|
+
raise ValueError(
|
|
834
|
+
"PolarAxisRegularScalarRule3D requires an even global angle "
|
|
835
|
+
f"count; got global_angle={global_angle}"
|
|
836
|
+
)
|
|
837
|
+
if global_angle != angle_count * local_angle:
|
|
838
|
+
raise ValueError(
|
|
839
|
+
"PolarAxisRegularScalarRule3D requires equal angle sharding; "
|
|
840
|
+
f"got global_angle={global_angle}, angle_count={angle_count}, "
|
|
841
|
+
f"local_angle={local_angle}"
|
|
842
|
+
)
|
|
843
|
+
if angle_count > 1 and self.angle_axis_name is None:
|
|
844
|
+
raise ValueError(
|
|
845
|
+
"PolarAxisRegularScalarRule3D requires angle_axis_name when "
|
|
846
|
+
"the angle axis is sharded"
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
shard_shift, local_shift = divmod(global_angle // 2, local_angle)
|
|
850
|
+
if angle_count == 1:
|
|
851
|
+
values = self._fill_local_angle(field_halo, domain, side, local_shift)
|
|
852
|
+
else:
|
|
853
|
+
values = self._fill_distributed_angle(
|
|
854
|
+
field_halo,
|
|
855
|
+
domain,
|
|
856
|
+
side=side,
|
|
857
|
+
shard_shift=shard_shift,
|
|
858
|
+
local_shift=local_shift,
|
|
859
|
+
)
|
|
860
|
+
return self._write_radial_ghost(
|
|
861
|
+
field_halo,
|
|
862
|
+
domain,
|
|
863
|
+
side=side,
|
|
864
|
+
values=values,
|
|
865
|
+
do_side=do_side,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
def _radial_mirror_source(self, field_halo, domain, *, side: str):
|
|
869
|
+
h = int(domain.layout.halo_width)
|
|
870
|
+
nx, ny, nz = domain.layout.owned_shape
|
|
871
|
+
j = slice(h, h + ny)
|
|
872
|
+
k = slice(h, h + nz)
|
|
873
|
+
if side == "lower":
|
|
874
|
+
return field_halo[h : h + h, j, k]
|
|
875
|
+
if side == "upper":
|
|
876
|
+
return field_halo[h + nx - h : h + nx, j, k][::-1, :, :]
|
|
877
|
+
raise ValueError(f"side must be 'lower' or 'upper', got {side!r}")
|
|
878
|
+
|
|
879
|
+
def _write_radial_ghost(
|
|
880
|
+
self,
|
|
881
|
+
field_halo,
|
|
882
|
+
domain,
|
|
883
|
+
*,
|
|
884
|
+
side: str,
|
|
885
|
+
values,
|
|
886
|
+
do_side,
|
|
887
|
+
):
|
|
888
|
+
h = int(domain.layout.halo_width)
|
|
889
|
+
nx, ny, nz = domain.layout.owned_shape
|
|
890
|
+
j = slice(h, h + ny)
|
|
891
|
+
k = slice(h, h + nz)
|
|
892
|
+
if side == "lower":
|
|
893
|
+
index = (slice(0, h), j, k)
|
|
894
|
+
old = field_halo[index]
|
|
895
|
+
return field_halo.at[index].set(
|
|
896
|
+
jnp.where(do_side, values[::-1, :, :], old)
|
|
897
|
+
)
|
|
898
|
+
if side == "upper":
|
|
899
|
+
index = (slice(h + nx, h + nx + h), j, k)
|
|
900
|
+
old = field_halo[index]
|
|
901
|
+
return field_halo.at[index].set(jnp.where(do_side, values, old))
|
|
902
|
+
raise ValueError(f"side must be 'lower' or 'upper', got {side!r}")
|
|
903
|
+
|
|
904
|
+
def _fill_local_angle(self, field_halo, domain, side, local_shift):
|
|
905
|
+
source = self._radial_mirror_source(field_halo, domain, side=side)
|
|
906
|
+
return jnp.roll(source, shift=-int(local_shift), axis=1)
|
|
907
|
+
|
|
908
|
+
def _fill_distributed_angle(self, field_halo, domain, *, side, shard_shift, local_shift):
|
|
909
|
+
count = int(domain.shard_spec.shard_counts[self.angle_axis])
|
|
910
|
+
source = self._radial_mirror_source(field_halo, domain, side=side)
|
|
911
|
+
shift = int(shard_shift) % count
|
|
912
|
+
received = lax.ppermute(
|
|
913
|
+
source,
|
|
914
|
+
axis_name=self.angle_axis_name,
|
|
915
|
+
perm=[
|
|
916
|
+
(source_id, (source_id - shift) % count)
|
|
917
|
+
for source_id in range(count)
|
|
918
|
+
],
|
|
919
|
+
)
|
|
920
|
+
if int(local_shift) != 0:
|
|
921
|
+
received = jnp.roll(received, shift=-int(local_shift), axis=1)
|
|
922
|
+
return received
|
|
923
|
+
|
|
924
|
+
def tree_flatten(self):
|
|
925
|
+
return (), (
|
|
926
|
+
self.angle_axis_name,
|
|
927
|
+
self.radial_axis,
|
|
928
|
+
self.angle_axis,
|
|
929
|
+
self.passive_axis,
|
|
930
|
+
self.fill_lower,
|
|
931
|
+
self.fill_upper,
|
|
932
|
+
self.require_even_global_angle,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
@classmethod
|
|
936
|
+
def tree_unflatten(cls, aux_data, children):
|
|
937
|
+
del children
|
|
938
|
+
return cls(*aux_data)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
@_pytree_base
|
|
942
|
+
@dataclass(frozen=True)
|
|
943
|
+
class PolarAxisRegularVectorRule3D(_DataclassPyTreeMixin):
|
|
944
|
+
"""Fill polar/axis-regularity halos for contravariant 3-vectors.
|
|
945
|
+
|
|
946
|
+
The first three axes of ``field_halo`` are spatial and the final axis has
|
|
947
|
+
length three. The rule performs the same radial mirror, angular shift, and
|
|
948
|
+
optional inter-shard permutation as the scalar polar rule, then applies
|
|
949
|
+
``component_transform``:
|
|
950
|
+
|
|
951
|
+
``V_target^i = T^i_j V_source^j``
|
|
952
|
+
|
|
953
|
+
For contravariant logical components, ``T`` should be the Jacobian of the
|
|
954
|
+
target logical coordinates with respect to the source logical coordinates.
|
|
955
|
+
A constant ``(3, 3)`` transform is supported, as is a transform whose
|
|
956
|
+
leading spatial dimensions broadcast to the target slab.
|
|
957
|
+
|
|
958
|
+
This rule is intentionally separate from
|
|
959
|
+
:class:`PolarAxisRegularScalarRule3D`; applying scalar polar regularity to
|
|
960
|
+
a vector field would copy the components without the required transform.
|
|
961
|
+
"""
|
|
962
|
+
|
|
963
|
+
axis: int
|
|
964
|
+
side: str
|
|
965
|
+
angular_axis: int
|
|
966
|
+
mesh_axis_name: str | None
|
|
967
|
+
source_shard_offset: int
|
|
968
|
+
local_shift_cells: int
|
|
969
|
+
component_transform: jnp.ndarray
|
|
970
|
+
halo_width: int | None = None
|
|
971
|
+
|
|
972
|
+
def __post_init__(self) -> None:
|
|
973
|
+
axis = int(self.axis)
|
|
974
|
+
angular_axis = int(self.angular_axis)
|
|
975
|
+
if axis not in (0, 1, 2):
|
|
976
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
977
|
+
if angular_axis not in (0, 1, 2):
|
|
978
|
+
raise ValueError(
|
|
979
|
+
f"angular_axis must be 0, 1, or 2, got {angular_axis}"
|
|
980
|
+
)
|
|
981
|
+
if axis == angular_axis:
|
|
982
|
+
raise ValueError(
|
|
983
|
+
"angular_axis should be distinct from the polar/axis coordinate"
|
|
984
|
+
)
|
|
985
|
+
if self.side not in ("lower", "upper"):
|
|
986
|
+
raise ValueError(
|
|
987
|
+
f"side must be 'lower' or 'upper', got {self.side!r}"
|
|
988
|
+
)
|
|
989
|
+
if self.mesh_axis_name is not None and not isinstance(
|
|
990
|
+
self.mesh_axis_name, str
|
|
991
|
+
):
|
|
992
|
+
raise TypeError("mesh_axis_name must be a string or None")
|
|
993
|
+
|
|
994
|
+
component_transform = jnp.asarray(
|
|
995
|
+
self.component_transform,
|
|
996
|
+
dtype=jnp.float64,
|
|
997
|
+
)
|
|
998
|
+
if component_transform.ndim < 2 or component_transform.shape[-2:] != (3, 3):
|
|
999
|
+
raise ValueError(
|
|
1000
|
+
"component_transform must have shape (3, 3) or a leading-spatial "
|
|
1001
|
+
f"shape ending in (3, 3), got {component_transform.shape}"
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
halo_width = None if self.halo_width is None else int(self.halo_width)
|
|
1005
|
+
if halo_width is not None and halo_width < 0:
|
|
1006
|
+
raise ValueError(f"halo_width must be nonnegative, got {halo_width}")
|
|
1007
|
+
|
|
1008
|
+
object.__setattr__(self, "axis", axis)
|
|
1009
|
+
object.__setattr__(self, "angular_axis", angular_axis)
|
|
1010
|
+
object.__setattr__(self, "source_shard_offset", int(self.source_shard_offset))
|
|
1011
|
+
object.__setattr__(self, "local_shift_cells", int(self.local_shift_cells))
|
|
1012
|
+
object.__setattr__(self, "component_transform", component_transform)
|
|
1013
|
+
object.__setattr__(self, "halo_width", halo_width)
|
|
1014
|
+
|
|
1015
|
+
def __call__(
|
|
1016
|
+
self,
|
|
1017
|
+
field_halo: jnp.ndarray,
|
|
1018
|
+
domain: LocalDomain3D,
|
|
1019
|
+
) -> jnp.ndarray:
|
|
1020
|
+
if not isinstance(domain, LocalDomain3D):
|
|
1021
|
+
raise TypeError(
|
|
1022
|
+
"PolarAxisRegularVectorRule3D requires LocalDomain3D, "
|
|
1023
|
+
f"got {type(domain).__name__}"
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
field_halo = _validate_halo_spatial_prefix(field_halo, domain)
|
|
1027
|
+
if field_halo.ndim != 4 or field_halo.shape[-1] != 3:
|
|
1028
|
+
raise ValueError(
|
|
1029
|
+
"PolarAxisRegularVectorRule3D requires a field with shape "
|
|
1030
|
+
"cell_halo_shape + (3,), "
|
|
1031
|
+
f"got {field_halo.shape}"
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
axis = self.axis
|
|
1035
|
+
angular_axis = self.angular_axis
|
|
1036
|
+
spec = domain.shard_spec
|
|
1037
|
+
if spec.shard_counts[axis] > 1 and domain.mesh_axis_names[axis] is None:
|
|
1038
|
+
raise ValueError(
|
|
1039
|
+
"PolarAxisRegularVectorRule3D requires a mesh axis name for a "
|
|
1040
|
+
"decomposed polar axis"
|
|
1041
|
+
)
|
|
1042
|
+
if (
|
|
1043
|
+
spec.lower_side_kind(angular_axis) != SIDE_SIMPLE_PERIODIC
|
|
1044
|
+
or spec.upper_side_kind(angular_axis) != SIDE_SIMPLE_PERIODIC
|
|
1045
|
+
):
|
|
1046
|
+
raise ValueError(
|
|
1047
|
+
"PolarAxisRegularVectorRule3D requires SIDE_SIMPLE_PERIODIC "
|
|
1048
|
+
"on both global angular sides"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
side_kind = (
|
|
1052
|
+
spec.lower_side_kind(axis)
|
|
1053
|
+
if self.side == "lower"
|
|
1054
|
+
else spec.upper_side_kind(axis)
|
|
1055
|
+
)
|
|
1056
|
+
if side_kind != SIDE_AXIS_REGULAR:
|
|
1057
|
+
raise ValueError(
|
|
1058
|
+
"PolarAxisRegularVectorRule3D requires SIDE_AXIS_REGULAR on "
|
|
1059
|
+
f"the selected {self.side} side of axis {axis}"
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
h = int(domain.layout.halo_width if self.halo_width is None else self.halo_width)
|
|
1063
|
+
if h == 0:
|
|
1064
|
+
return field_halo
|
|
1065
|
+
if h > domain.layout.halo_width:
|
|
1066
|
+
raise ValueError(
|
|
1067
|
+
"PolarAxisRegularVectorRule3D.halo_width cannot exceed the "
|
|
1068
|
+
f"field halo width; got {h}, layout halo width={domain.layout.halo_width}"
|
|
1069
|
+
)
|
|
1070
|
+
if h > domain.owned_shape[axis]:
|
|
1071
|
+
raise ValueError(
|
|
1072
|
+
"PolarAxisRegularVectorRule3D requires halo_width no larger "
|
|
1073
|
+
f"than the local polar extent; got h={h}, extent={domain.owned_shape[axis]}"
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
global_angle = int(spec.global_shape[angular_axis])
|
|
1077
|
+
local_angle = int(domain.owned_shape[angular_axis])
|
|
1078
|
+
angle_count = int(spec.shard_counts[angular_axis])
|
|
1079
|
+
if global_angle % 2:
|
|
1080
|
+
raise ValueError(
|
|
1081
|
+
"PolarAxisRegularVectorRule3D requires an even global angular "
|
|
1082
|
+
f"count; got {global_angle}"
|
|
1083
|
+
)
|
|
1084
|
+
if global_angle != angle_count * local_angle:
|
|
1085
|
+
raise ValueError(
|
|
1086
|
+
"PolarAxisRegularVectorRule3D requires equal angular sharding; "
|
|
1087
|
+
f"got global_angle={global_angle}, angle_count={angle_count}, "
|
|
1088
|
+
f"local_angle={local_angle}"
|
|
1089
|
+
)
|
|
1090
|
+
if angle_count > 1 and self.mesh_axis_name is None:
|
|
1091
|
+
raise ValueError(
|
|
1092
|
+
"PolarAxisRegularVectorRule3D requires mesh_axis_name when the "
|
|
1093
|
+
"angular axis is sharded"
|
|
1094
|
+
)
|
|
1095
|
+
if (
|
|
1096
|
+
angle_count > 1
|
|
1097
|
+
and self.mesh_axis_name != domain.mesh_axis_names[angular_axis]
|
|
1098
|
+
):
|
|
1099
|
+
raise ValueError(
|
|
1100
|
+
"PolarAxisRegularVectorRule3D.mesh_axis_name must match the "
|
|
1101
|
+
f"domain mesh name for angular axis {angular_axis}; "
|
|
1102
|
+
f"got rule={self.mesh_axis_name!r}, "
|
|
1103
|
+
f"domain={domain.mesh_axis_names[angular_axis]!r}"
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
ext = tuple(int(value) for value in domain.layout.owned_shape)
|
|
1107
|
+
spatial_owned = tuple(slice(h, h + size) for size in ext)
|
|
1108
|
+
source_spatial = list(spatial_owned)
|
|
1109
|
+
target_spatial = list(spatial_owned)
|
|
1110
|
+
if self.side == "lower":
|
|
1111
|
+
do_write = domain.runtime_touches_lower(axis)
|
|
1112
|
+
source_spatial[axis] = slice(h, h + h)
|
|
1113
|
+
target_spatial[axis] = slice(0, h)
|
|
1114
|
+
else:
|
|
1115
|
+
do_write = domain.runtime_touches_upper(axis)
|
|
1116
|
+
source_spatial[axis] = slice(h + ext[axis] - h, h + ext[axis])
|
|
1117
|
+
target_spatial[axis] = slice(h + ext[axis], h + ext[axis] + h)
|
|
1118
|
+
|
|
1119
|
+
trailing = _trailing_slices(field_halo.ndim)
|
|
1120
|
+
source_index = tuple(source_spatial) + trailing
|
|
1121
|
+
target_index = tuple(target_spatial) + trailing
|
|
1122
|
+
recv = field_halo[source_index]
|
|
1123
|
+
|
|
1124
|
+
if angle_count > 1:
|
|
1125
|
+
recv = lax.ppermute(
|
|
1126
|
+
recv,
|
|
1127
|
+
axis_name=self.mesh_axis_name,
|
|
1128
|
+
perm=[
|
|
1129
|
+
(
|
|
1130
|
+
source,
|
|
1131
|
+
(source + self.source_shard_offset) % angle_count,
|
|
1132
|
+
)
|
|
1133
|
+
for source in range(angle_count)
|
|
1134
|
+
],
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
if self.local_shift_cells:
|
|
1138
|
+
recv = jnp.roll(
|
|
1139
|
+
recv,
|
|
1140
|
+
shift=-self.local_shift_cells,
|
|
1141
|
+
axis=angular_axis,
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
# Match the scalar rule's radial ordering. The lower and upper source
|
|
1145
|
+
# slabs are both reversed before being written into the corresponding
|
|
1146
|
+
# ghost slab; this is essential when halo_width > 1.
|
|
1147
|
+
recv = jnp.flip(recv, axis=axis)
|
|
1148
|
+
|
|
1149
|
+
transform = jnp.asarray(self.component_transform, dtype=field_halo.dtype)
|
|
1150
|
+
target_transform_shape = recv.shape[:-1] + (3, 3)
|
|
1151
|
+
try:
|
|
1152
|
+
transform = jnp.broadcast_to(transform, target_transform_shape)
|
|
1153
|
+
except ValueError as exc:
|
|
1154
|
+
raise ValueError(
|
|
1155
|
+
"component_transform must be broadcastable to the target polar "
|
|
1156
|
+
f"slab matrix shape {target_transform_shape}, got {transform.shape}"
|
|
1157
|
+
) from exc
|
|
1158
|
+
recv = jnp.einsum("...ij,...j->...i", transform, recv)
|
|
1159
|
+
|
|
1160
|
+
old = field_halo[target_index]
|
|
1161
|
+
return field_halo.at[target_index].set(jnp.where(do_write, recv, old))
|
|
1162
|
+
|
|
1163
|
+
def tree_flatten(self):
|
|
1164
|
+
return (self.component_transform,), (
|
|
1165
|
+
self.axis,
|
|
1166
|
+
self.side,
|
|
1167
|
+
self.angular_axis,
|
|
1168
|
+
self.mesh_axis_name,
|
|
1169
|
+
self.source_shard_offset,
|
|
1170
|
+
self.local_shift_cells,
|
|
1171
|
+
self.halo_width,
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
@classmethod
|
|
1175
|
+
def tree_unflatten(cls, aux_data, children):
|
|
1176
|
+
(
|
|
1177
|
+
axis,
|
|
1178
|
+
side,
|
|
1179
|
+
angular_axis,
|
|
1180
|
+
mesh_axis_name,
|
|
1181
|
+
source_shard_offset,
|
|
1182
|
+
local_shift_cells,
|
|
1183
|
+
halo_width,
|
|
1184
|
+
) = aux_data
|
|
1185
|
+
(component_transform,) = children
|
|
1186
|
+
return cls(
|
|
1187
|
+
axis=axis,
|
|
1188
|
+
side=side,
|
|
1189
|
+
angular_axis=angular_axis,
|
|
1190
|
+
mesh_axis_name=mesh_axis_name,
|
|
1191
|
+
source_shard_offset=source_shard_offset,
|
|
1192
|
+
local_shift_cells=local_shift_cells,
|
|
1193
|
+
component_transform=component_transform,
|
|
1194
|
+
halo_width=halo_width,
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def make_default_topology_halo_filler_3d(
|
|
1199
|
+
*,
|
|
1200
|
+
angle_axis_name: str | None = None,
|
|
1201
|
+
radial_axis: int = 0,
|
|
1202
|
+
theta_axis: int = 1,
|
|
1203
|
+
radial_axis_lower_regular: bool = True,
|
|
1204
|
+
radial_axis_upper_regular: bool = False,
|
|
1205
|
+
fill_periodic_axes: tuple[bool, bool, bool] = (True, True, True),
|
|
1206
|
+
) -> TopologyHaloFiller3D:
|
|
1207
|
+
"""Build the default local periodic and scalar radial topology pipeline."""
|
|
1208
|
+
|
|
1209
|
+
return TopologyHaloFiller3D(
|
|
1210
|
+
rules=(
|
|
1211
|
+
LocalPeriodicTopologyRule3D(fill_axes=fill_periodic_axes),
|
|
1212
|
+
PolarAxisRegularScalarRule3D(
|
|
1213
|
+
angle_axis_name=angle_axis_name,
|
|
1214
|
+
radial_axis=radial_axis,
|
|
1215
|
+
angle_axis=theta_axis,
|
|
1216
|
+
fill_lower=radial_axis_lower_regular,
|
|
1217
|
+
fill_upper=radial_axis_upper_regular,
|
|
1218
|
+
),
|
|
1219
|
+
)
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
@_pytree_base
|
|
1224
|
+
@dataclass(frozen=True)
|
|
1225
|
+
class GhostFillWeights1D(_DataclassPyTreeMixin):
|
|
1226
|
+
"""Weights for one coordinate direction and one ghost-fill rule.
|
|
1227
|
+
|
|
1228
|
+
``owned_weights[r, m]`` multiplies the ``m``-th owned cell inward from
|
|
1229
|
+
the boundary when constructing ghost layer ``r``. ``bc_weights[r]``
|
|
1230
|
+
multiplies the supplied boundary value. The weights are deliberately
|
|
1231
|
+
supplied by the caller so they can encode nonuniform spacing and any
|
|
1232
|
+
desired reconstruction order.
|
|
1233
|
+
"""
|
|
1234
|
+
|
|
1235
|
+
owned_weights: jnp.ndarray
|
|
1236
|
+
bc_weights: jnp.ndarray
|
|
1237
|
+
|
|
1238
|
+
def __post_init__(self) -> None:
|
|
1239
|
+
owned_weights = jnp.asarray(self.owned_weights, dtype=jnp.float64)
|
|
1240
|
+
bc_weights = jnp.asarray(self.bc_weights, dtype=jnp.float64)
|
|
1241
|
+
if owned_weights.ndim != 2:
|
|
1242
|
+
raise ValueError(
|
|
1243
|
+
"GhostFillWeights1D.owned_weights must have shape "
|
|
1244
|
+
"(halo_width, stencil_width)"
|
|
1245
|
+
)
|
|
1246
|
+
if bc_weights.ndim != 1:
|
|
1247
|
+
raise ValueError(
|
|
1248
|
+
"GhostFillWeights1D.bc_weights must have shape (halo_width,)"
|
|
1249
|
+
)
|
|
1250
|
+
if bc_weights.shape[0] != owned_weights.shape[0]:
|
|
1251
|
+
raise ValueError(
|
|
1252
|
+
"GhostFillWeights1D.bc_weights length must match halo_width"
|
|
1253
|
+
)
|
|
1254
|
+
object.__setattr__(self, "owned_weights", owned_weights)
|
|
1255
|
+
object.__setattr__(self, "bc_weights", bc_weights)
|
|
1256
|
+
|
|
1257
|
+
@property
|
|
1258
|
+
def halo_width(self) -> int:
|
|
1259
|
+
return int(self.owned_weights.shape[0])
|
|
1260
|
+
|
|
1261
|
+
@property
|
|
1262
|
+
def stencil_width(self) -> int:
|
|
1263
|
+
return int(self.owned_weights.shape[1])
|
|
1264
|
+
|
|
1265
|
+
def tree_flatten(self):
|
|
1266
|
+
return (self.owned_weights, self.bc_weights), None
|
|
1267
|
+
|
|
1268
|
+
@classmethod
|
|
1269
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1270
|
+
owned_weights, bc_weights = children
|
|
1271
|
+
return cls(
|
|
1272
|
+
owned_weights=owned_weights,
|
|
1273
|
+
bc_weights=bc_weights,
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _validate_axis_weights(
|
|
1278
|
+
weights: tuple[GhostFillWeights1D, GhostFillWeights1D, GhostFillWeights1D],
|
|
1279
|
+
name: str,
|
|
1280
|
+
) -> tuple[GhostFillWeights1D, GhostFillWeights1D, GhostFillWeights1D]:
|
|
1281
|
+
weights = tuple(weights)
|
|
1282
|
+
if len(weights) != 3:
|
|
1283
|
+
raise ValueError(f"{name} must contain one weight set per axis")
|
|
1284
|
+
if not all(isinstance(value, GhostFillWeights1D) for value in weights):
|
|
1285
|
+
raise TypeError(f"{name} entries must be GhostFillWeights1D instances")
|
|
1286
|
+
return weights # type: ignore[return-value]
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
@_pytree_base
|
|
1290
|
+
@dataclass(frozen=True)
|
|
1291
|
+
class PhysicalGhostCellFiller3D(_DataclassPyTreeMixin):
|
|
1292
|
+
"""Fill regular-coordinate physical face ghost slabs.
|
|
1293
|
+
|
|
1294
|
+
The BC payload is passed to ``__call__`` and is therefore dynamic: it may
|
|
1295
|
+
depend on the evolving state. The three weight collections are static
|
|
1296
|
+
reconstruction configuration, with one entry for each coordinate axis.
|
|
1297
|
+
|
|
1298
|
+
This stage fills only slabs with exactly one ghost-coordinate direction.
|
|
1299
|
+
It intentionally does not fill halo edge or corner pieces (cells with two
|
|
1300
|
+
or three ghost-coordinate directions); current operators do not require
|
|
1301
|
+
those values. A future topology/corner stage can own that policy.
|
|
1302
|
+
|
|
1303
|
+
Only ``BC_DIRICHLET`` and ``BC_NEUMANN`` are materialized. Flux-level BCs
|
|
1304
|
+
are not scalar ghost-cell rules and remain available to flux operators.
|
|
1305
|
+
"""
|
|
1306
|
+
|
|
1307
|
+
dirichlet: tuple[GhostFillWeights1D, GhostFillWeights1D, GhostFillWeights1D]
|
|
1308
|
+
neumann_lower: tuple[GhostFillWeights1D, GhostFillWeights1D, GhostFillWeights1D]
|
|
1309
|
+
neumann_upper: tuple[GhostFillWeights1D, GhostFillWeights1D, GhostFillWeights1D]
|
|
1310
|
+
|
|
1311
|
+
def __post_init__(self) -> None:
|
|
1312
|
+
dirichlet = _validate_axis_weights(self.dirichlet, "dirichlet")
|
|
1313
|
+
neumann_lower = _validate_axis_weights(self.neumann_lower, "neumann_lower")
|
|
1314
|
+
neumann_upper = _validate_axis_weights(self.neumann_upper, "neumann_upper")
|
|
1315
|
+
for axis in range(3):
|
|
1316
|
+
h = dirichlet[axis].halo_width
|
|
1317
|
+
if neumann_lower[axis].halo_width != h:
|
|
1318
|
+
raise ValueError(f"neumann_lower[{axis}] halo_width must match dirichlet")
|
|
1319
|
+
if neumann_upper[axis].halo_width != h:
|
|
1320
|
+
raise ValueError(f"neumann_upper[{axis}] halo_width must match dirichlet")
|
|
1321
|
+
object.__setattr__(self, "dirichlet", dirichlet)
|
|
1322
|
+
object.__setattr__(self, "neumann_lower", neumann_lower)
|
|
1323
|
+
object.__setattr__(self, "neumann_upper", neumann_upper)
|
|
1324
|
+
|
|
1325
|
+
def __call__(
|
|
1326
|
+
self,
|
|
1327
|
+
field_halo: jnp.ndarray,
|
|
1328
|
+
domain: LocalDomain3D,
|
|
1329
|
+
face_bc: LocalBoundaryFaceBC3D | None,
|
|
1330
|
+
) -> jnp.ndarray:
|
|
1331
|
+
if face_bc is None:
|
|
1332
|
+
return field_halo
|
|
1333
|
+
if not isinstance(domain, LocalDomain3D):
|
|
1334
|
+
raise TypeError("PhysicalGhostCellFiller3D.domain must be a LocalDomain3D")
|
|
1335
|
+
if not isinstance(face_bc, LocalBoundaryFaceBC3D):
|
|
1336
|
+
raise TypeError(
|
|
1337
|
+
"PhysicalGhostCellFiller3D.face_bc must be a LocalBoundaryFaceBC3D"
|
|
1338
|
+
)
|
|
1339
|
+
if face_bc.layout != domain.layout:
|
|
1340
|
+
raise ValueError("face_bc and domain must share the same HaloLayout3D")
|
|
1341
|
+
|
|
1342
|
+
field_halo = jnp.asarray(field_halo)
|
|
1343
|
+
expected_shape = domain.layout.cell_halo_shape
|
|
1344
|
+
if field_halo.shape != expected_shape:
|
|
1345
|
+
raise ValueError(f"field_halo must have shape {expected_shape}, got {field_halo.shape}")
|
|
1346
|
+
|
|
1347
|
+
h = int(domain.layout.halo_width)
|
|
1348
|
+
if h == 0:
|
|
1349
|
+
return field_halo
|
|
1350
|
+
for axis in range(3):
|
|
1351
|
+
if self.dirichlet[axis].halo_width != h:
|
|
1352
|
+
raise ValueError(f"ghost-fill weights[{axis}] halo_width must match domain halo_width")
|
|
1353
|
+
if self.dirichlet[axis].stencil_width > domain.owned_shape[axis]:
|
|
1354
|
+
raise ValueError(f"dirichlet[{axis}] stencil exceeds owned extent")
|
|
1355
|
+
if self.neumann_lower[axis].stencil_width > domain.owned_shape[axis]:
|
|
1356
|
+
raise ValueError(f"neumann_lower[{axis}] stencil exceeds owned extent")
|
|
1357
|
+
if self.neumann_upper[axis].stencil_width > domain.owned_shape[axis]:
|
|
1358
|
+
raise ValueError(f"neumann_upper[{axis}] stencil exceeds owned extent")
|
|
1359
|
+
|
|
1360
|
+
result = field_halo
|
|
1361
|
+
for axis in range(3):
|
|
1362
|
+
result = self._fill_axis_side(result, domain, face_bc, axis, "lower")
|
|
1363
|
+
result = self._fill_axis_side(result, domain, face_bc, axis, "upper")
|
|
1364
|
+
return result
|
|
1365
|
+
|
|
1366
|
+
def _fill_axis_side(self, field_halo, domain, face_bc, axis, side):
|
|
1367
|
+
if side == "lower":
|
|
1368
|
+
side_active = domain.runtime_has_physical_lower(axis)
|
|
1369
|
+
elif side == "upper":
|
|
1370
|
+
side_active = domain.runtime_has_physical_upper(axis)
|
|
1371
|
+
else:
|
|
1372
|
+
raise ValueError(f"side must be 'lower' or 'upper', got {side!r}")
|
|
1373
|
+
|
|
1374
|
+
kind, value, mask = self._bc_plane(face_bc, axis, side)
|
|
1375
|
+
old = self._ghost_slab(field_halo, domain.layout, axis, side)
|
|
1376
|
+
stencil_width = max(
|
|
1377
|
+
self.dirichlet[axis].stencil_width,
|
|
1378
|
+
(self.neumann_lower if side == "lower" else self.neumann_upper)[axis].stencil_width,
|
|
1379
|
+
)
|
|
1380
|
+
owned = self._owned_stencil(field_halo, domain.layout, axis, side, stencil_width)
|
|
1381
|
+
dghost = self._apply(self.dirichlet[axis], owned[: self.dirichlet[axis].stencil_width], value)
|
|
1382
|
+
nweights = (self.neumann_lower if side == "lower" else self.neumann_upper)[axis]
|
|
1383
|
+
nghost = self._apply(nweights, owned[: nweights.stencil_width], value)
|
|
1384
|
+
active_mask = side_active & mask
|
|
1385
|
+
new = jnp.where(
|
|
1386
|
+
(active_mask & (kind == BC_DIRICHLET))[None, ...],
|
|
1387
|
+
dghost,
|
|
1388
|
+
old,
|
|
1389
|
+
)
|
|
1390
|
+
new = jnp.where(
|
|
1391
|
+
(active_mask & (kind == BC_NEUMANN))[None, ...],
|
|
1392
|
+
nghost,
|
|
1393
|
+
new,
|
|
1394
|
+
)
|
|
1395
|
+
return self._set_ghost_slab(field_halo, new, domain.layout, axis, side)
|
|
1396
|
+
|
|
1397
|
+
@staticmethod
|
|
1398
|
+
def _apply(weights, owned, value):
|
|
1399
|
+
ghost = jnp.tensordot(weights.owned_weights, owned, axes=((1,), (0,)))
|
|
1400
|
+
return ghost + weights.bc_weights.reshape((weights.halo_width,) + (1,) * value.ndim) * value[None, ...]
|
|
1401
|
+
|
|
1402
|
+
@staticmethod
|
|
1403
|
+
def _bc_plane(face_bc, axis, side):
|
|
1404
|
+
index = 0 if side == "lower" else -1
|
|
1405
|
+
if axis == 0:
|
|
1406
|
+
return face_bc.kind_x[index], face_bc.value_x[index], face_bc.mask_x[index]
|
|
1407
|
+
if axis == 1:
|
|
1408
|
+
return face_bc.kind_y[:, index, :], face_bc.value_y[:, index, :], face_bc.mask_y[:, index, :]
|
|
1409
|
+
return face_bc.kind_z[:, :, index], face_bc.value_z[:, :, index], face_bc.mask_z[:, :, index]
|
|
1410
|
+
|
|
1411
|
+
@staticmethod
|
|
1412
|
+
def _owned_stencil(field, layout, axis, side, width):
|
|
1413
|
+
h = layout.halo_width
|
|
1414
|
+
ext = layout.owned_shape
|
|
1415
|
+
slices = [slice(h, h + n) for n in ext]
|
|
1416
|
+
if side == "lower":
|
|
1417
|
+
slices[axis] = slice(h, h + width)
|
|
1418
|
+
slab = field[tuple(slices)]
|
|
1419
|
+
else:
|
|
1420
|
+
slices[axis] = slice(h + ext[axis] - width, h + ext[axis])
|
|
1421
|
+
slab = field[tuple(slices)]
|
|
1422
|
+
slab = jnp.flip(slab, axis=axis)
|
|
1423
|
+
return jnp.moveaxis(slab, axis, 0)
|
|
1424
|
+
|
|
1425
|
+
@staticmethod
|
|
1426
|
+
def _ghost_slab(field, layout, axis, side):
|
|
1427
|
+
h = layout.halo_width
|
|
1428
|
+
ext = layout.owned_shape
|
|
1429
|
+
slices = [slice(h, h + n) for n in ext]
|
|
1430
|
+
slices[axis] = slice(0, h) if side == "lower" else slice(h + ext[axis], h + ext[axis] + h)
|
|
1431
|
+
slab = field[tuple(slices)]
|
|
1432
|
+
if side == "lower":
|
|
1433
|
+
slab = jnp.flip(slab, axis=axis)
|
|
1434
|
+
return jnp.moveaxis(slab, axis, 0)
|
|
1435
|
+
|
|
1436
|
+
@staticmethod
|
|
1437
|
+
def _set_ghost_slab(field, slab, layout, axis, side):
|
|
1438
|
+
h = layout.halo_width
|
|
1439
|
+
ext = layout.owned_shape
|
|
1440
|
+
raw = jnp.moveaxis(slab, 0, axis)
|
|
1441
|
+
if side == "lower":
|
|
1442
|
+
raw = jnp.flip(raw, axis=axis)
|
|
1443
|
+
slices = [slice(h, h + n) for n in ext]
|
|
1444
|
+
slices[axis] = slice(0, h) if side == "lower" else slice(h + ext[axis], h + ext[axis] + h)
|
|
1445
|
+
return field.at[tuple(slices)].set(raw)
|
|
1446
|
+
|
|
1447
|
+
def tree_flatten(self):
|
|
1448
|
+
return (self.dirichlet, self.neumann_lower, self.neumann_upper), None
|
|
1449
|
+
|
|
1450
|
+
@classmethod
|
|
1451
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
1452
|
+
return cls(*children)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
@_pytree_base
|
|
1456
|
+
@dataclass(frozen=True)
|
|
1457
|
+
class PreparedLocalState3D(_DataclassPyTreeMixin):
|
|
1458
|
+
"""Fully prepared local state and its model-shaped boundary payloads."""
|
|
1459
|
+
|
|
1460
|
+
state_halo: FciModelState
|
|
1461
|
+
boundary_data: LocalBoundaryData3D
|
|
1462
|
+
|
|
1463
|
+
def __post_init__(self) -> None:
|
|
1464
|
+
if not isinstance(self.state_halo, FciModelState):
|
|
1465
|
+
raise TypeError("PreparedLocalState3D.state_halo must be an FciModelState")
|
|
1466
|
+
if not isinstance(self.boundary_data, LocalBoundaryData3D):
|
|
1467
|
+
raise TypeError(
|
|
1468
|
+
"PreparedLocalState3D.boundary_data must be a LocalBoundaryData3D"
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
@_pytree_base
|
|
1473
|
+
@dataclass(frozen=True)
|
|
1474
|
+
class LocalStateAndBoundaryPreparer3D(_DataclassPyTreeMixin):
|
|
1475
|
+
"""Prepare state halos, coupled BCs, and physical ghost cells in order.
|
|
1476
|
+
|
|
1477
|
+
The pre-BC stages operate field-by-field, while the boundary builder sees
|
|
1478
|
+
the complete topology-prepared state so it can construct coupled payloads.
|
|
1479
|
+
Physical ghost filling then applies the matching face BC to each field.
|
|
1480
|
+
Halo edges and corners remain governed by the individual field stages;
|
|
1481
|
+
this orchestrator does not add corner handling.
|
|
1482
|
+
"""
|
|
1483
|
+
|
|
1484
|
+
boundary_builder: LocalBoundaryConditionBuilder
|
|
1485
|
+
physical_ghost_filler: PhysicalGhostCellFiller3D
|
|
1486
|
+
halo_exchange: HaloExchange3D | None = None
|
|
1487
|
+
topology_filler: TopologyHaloFiller3D | None = None
|
|
1488
|
+
|
|
1489
|
+
def __post_init__(self) -> None:
|
|
1490
|
+
if not isinstance(self.boundary_builder, LocalBoundaryConditionBuilder):
|
|
1491
|
+
raise TypeError(
|
|
1492
|
+
"boundary_builder must be a LocalBoundaryConditionBuilder"
|
|
1493
|
+
)
|
|
1494
|
+
if not isinstance(self.physical_ghost_filler, PhysicalGhostCellFiller3D):
|
|
1495
|
+
raise TypeError(
|
|
1496
|
+
"physical_ghost_filler must be a PhysicalGhostCellFiller3D"
|
|
1497
|
+
)
|
|
1498
|
+
if self.halo_exchange is not None and not isinstance(
|
|
1499
|
+
self.halo_exchange, HaloExchange3D
|
|
1500
|
+
):
|
|
1501
|
+
raise TypeError("halo_exchange must be a HaloExchange3D or None")
|
|
1502
|
+
if self.topology_filler is not None and not isinstance(
|
|
1503
|
+
self.topology_filler, TopologyHaloFiller3D
|
|
1504
|
+
):
|
|
1505
|
+
raise TypeError(
|
|
1506
|
+
"topology_filler must be a TopologyHaloFiller3D or None"
|
|
1507
|
+
)
|
|
1508
|
+
|
|
1509
|
+
def __call__(
|
|
1510
|
+
self,
|
|
1511
|
+
state_owned: FciModelStateT,
|
|
1512
|
+
geometry: LocalFciGeometry3D,
|
|
1513
|
+
domain: LocalDomain3D,
|
|
1514
|
+
cut_wall_geometry: LocalCutWallGeometry3D | None = None,
|
|
1515
|
+
) -> PreparedLocalState3D:
|
|
1516
|
+
if not isinstance(state_owned, FciModelState):
|
|
1517
|
+
raise TypeError("state_owned must be an FciModelState instance")
|
|
1518
|
+
if not isinstance(domain, LocalDomain3D):
|
|
1519
|
+
raise TypeError("domain must be a LocalDomain3D instance")
|
|
1520
|
+
|
|
1521
|
+
state_halo = inject_owned_state_to_halo(state_owned, domain.layout)
|
|
1522
|
+
|
|
1523
|
+
if self.halo_exchange is not None:
|
|
1524
|
+
state_halo = state_halo.map_fields(
|
|
1525
|
+
lambda field_halo: self.halo_exchange(field_halo, domain)
|
|
1526
|
+
)
|
|
1527
|
+
if self.topology_filler is not None:
|
|
1528
|
+
state_halo = state_halo.map_fields(
|
|
1529
|
+
lambda field_halo: self.topology_filler(field_halo, domain)
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
boundary_data = self.boundary_builder(
|
|
1533
|
+
state_halo,
|
|
1534
|
+
geometry,
|
|
1535
|
+
domain,
|
|
1536
|
+
cut_wall_geometry,
|
|
1537
|
+
)
|
|
1538
|
+
if boundary_data.face_bc is None:
|
|
1539
|
+
raise ValueError(
|
|
1540
|
+
"boundary_builder must return face_bc for every state field "
|
|
1541
|
+
"before physical ghost preparation"
|
|
1542
|
+
)
|
|
1543
|
+
face_bc_bundle = boundary_data.face_bc
|
|
1544
|
+
assert_matching_field_names(state_halo, face_bc_bundle)
|
|
1545
|
+
|
|
1546
|
+
state_halo_full = state_halo.replace(
|
|
1547
|
+
**{
|
|
1548
|
+
name: self.physical_ghost_filler(
|
|
1549
|
+
getattr(state_halo, name),
|
|
1550
|
+
domain,
|
|
1551
|
+
getattr(face_bc_bundle, name),
|
|
1552
|
+
)
|
|
1553
|
+
for name in state_halo.field_names()
|
|
1554
|
+
}
|
|
1555
|
+
)
|
|
1556
|
+
return PreparedLocalState3D(
|
|
1557
|
+
state_halo=state_halo_full,
|
|
1558
|
+
boundary_data=boundary_data,
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
__all__ = [
|
|
1562
|
+
"FciCutWallValueEvaluator",
|
|
1563
|
+
"GhostFillWeights1D",
|
|
1564
|
+
"HaloExchange3D",
|
|
1565
|
+
"LocalFciCutWallValueEvaluator",
|
|
1566
|
+
"LocalPeriodicTopologyRule3D",
|
|
1567
|
+
"LocalStateAndBoundaryPreparer3D",
|
|
1568
|
+
"PreparedLocalState3D",
|
|
1569
|
+
"PhysicalGhostCellFiller3D",
|
|
1570
|
+
"PolarAxisRegularScalarRule3D",
|
|
1571
|
+
"PolarAxisRegularVectorRule3D",
|
|
1572
|
+
"RemoteFciDependencyExchange",
|
|
1573
|
+
"TopologyHaloFiller3D",
|
|
1574
|
+
"make_default_topology_halo_filler_3d",
|
|
1575
|
+
]
|