1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! # Module `operators`
//!
//! Spatial discretization operators — INV-2 invariant.
//!
//! ## Core principle (INV-2)
//!
//! Spatial discretization is decoupled from the rest of the engine through two
//! sibling traits — never a single generic-over-everything trait, and never a
//! scheme called directly from a consumer:
//!
//! - [`DiscreteOperator`] — raw differential quantities (`∂u/∂x`, `∂²u/∂x²`) that
//! never need physical parameters or context access; `dx` comes from the mesh
//! alone. Consumed transparently through [`crate::context::ContextCalculator`]
//! (e.g. `FDGradientCalculator`) — a model declares a required context
//! variable and never knows which scheme produced it (DD-012).
//! - [`FluxDivergenceOperator`] — the complete `∇·F(u, ∇u)` term, which does need
//! physical parameters (advection velocity, diffusion coefficient) that may be
//! space/time-varying. Carries `ctx: &ComputeContext` and `RequiresContext`
//! directly on `apply()`. Consumed by `DiscretizedModel<Op>` (DD-038, DD-039).
//!
//! `FluxDivergenceOperator` is a **sibling** trait, not a sub-trait of
//! `DiscreteOperator`: their `apply()` signatures diverge (with/without `ctx`),
//! so a sub-trait relationship could not cleanly reuse the parent method.
//! Forcing `ctx`/`RequiresContext` onto `DiscreteOperator` for every scheme was
//! considered and rejected (DD-012, amendment 2) — it would have imposed that
//! capability on FD, which never needs it.
//!
//! **Selection criterion for future schemes** (FEM, spectral methods, J20):
//! does the scheme need per-call access to physical context, or does it reduce
//! to a local differentiation from mesh data alone? The former implements
//! `FluxDivergenceOperator`; the latter implements `DiscreteOperator`.
//!
//! ## Planned implementations
//!
//! | Type | Scheme | Trait | Milestone |
//! |---|---|---|---|
//! | [`fd::UpwindGradient`], [`fd::CenteredGradient`], [`fd::CenteredLaplacian`] | FD | `DiscreteOperator` | v0.5.0 (#47) ✅ |
//! | [`fv::FVCenteredFlux`], [`fv::FVUpwindFlux`] | FV | `FluxDivergenceOperator` | v0.5.0 (#48) ✅ |
//! | [`weno::WENO3`], [`weno::WENO5`] | WENO | `FluxDivergenceOperator` | v0.5.0 (#49) ✅ |
//! | [`limiters::LimitedFlux`] (`MinMod`/`VanLeer`/`Superbee`) | MUSCL | `FluxDivergenceOperator` | v0.5.0 (#99) ✅ |
//! | [`adaptive::AdaptiveFlux`] (WENO3/limiter blend, Péclet-gated) | — | `FluxDivergenceOperator` | v0.5.0 (#99) ✅ |
//! | `FiniteElement` | FEM P1/P2 | TBD | J7 — v2.0 |
use crateBoundaryCondition;
use crateComputeContext;
use crateOxiflowError;
use crateContextValue;
use crateMesh;
use crateRequiresContext;
use DVector;
/// Raw spatial differential operator (FD family) — INV-2.
///
/// Computes a single differential quantity (`∂u/∂x`, `∂²u/∂x²`, ...) from a
/// field and a mesh, with no dependency on physical parameters or runtime
/// context: `dx` comes entirely from `mesh`. Consumed transparently through
/// [`crate::context::ContextCalculator`] (DD-012) — never called directly by a
/// temporal integrator.
///
/// Uses an associated type `MeshType`, not a generic parameter, per DD-012
/// amendment 1: each scheme implementation targets exactly one mesh family, so
/// a generic `DiscreteOperator<M: Mesh>` would add flexibility no consumer
/// needs, at the cost of object-safety (INV-4).
///
/// Distinct from [`FluxDivergenceOperator`] (DD-039), which computes the
/// complete `∇·F(u, ∇u)` term and does need context access — see the module
/// documentation above for the selection criterion between the two.
/// Complete flux-divergence spatial operator (FV/WENO family) — DD-039.
///
/// Computes the full `∇·F(u, ∇u)` term of the canonical PDE form, which —
/// unlike [`DiscreteOperator`] — requires knowledge of the physics of `F`
/// (advection velocity, diffusion coefficient, ...), potentially varying in
/// space or time. Carries [`RequiresContext`] and receives `ctx` directly on
/// `apply()`.
///
/// A **sibling** trait to `DiscreteOperator`, not a sub-trait: the two
/// `apply()` signatures diverge (with/without `ctx`), so a sub-trait
/// relationship could not reuse the parent method cleanly (DD-039, option B
/// rejected). Adding `ctx`/`RequiresContext` to `DiscreteOperator` itself was
/// also rejected (DD-012, amendment 2; DD-039, option A) — it would force
/// that capability onto FD schemes, which never need it.
///
/// Two consumption modes fall out of `RequiresContext` for free, with no new
/// mechanism: a scheme with only constant physical parameters returns
/// `required_variables() -> vec![]` and reads its own fields directly in
/// `apply()`; a scheme with space/time-varying parameters declares them as
/// [`crate::context::ContextVariable`]s in `required_variables()` and reads
/// them from `ctx`, resolved and ordered beforehand by `chain.rs` (DD-009)
/// exactly as for any `PhysicalModel`/`BoundaryCondition`.
///
/// Consumed by `DiscretizedModel<Op>` (DD-038), which binds `Op:
/// FluxDivergenceOperator` — an FD scheme cannot be plugged in there; that
/// becomes a compile error, not a silent runtime bug.
// ── FluxBoundary ──────────────────────────────────────────────────────────────
/// Boundary treatment shared by cell/face-based [`FluxDivergenceOperator`]
/// families (`operators::fv`, `operators::weno`) — both face the same
/// question: a finite `n`-node mesh only delimits `n−1` interior cells (FV)
/// or lacks enough neighbors for a wide stencil at the edges (WENO) unless
/// the domain wraps around.
///
/// Three families of treatment exist: periodic wrap (exact conservation by
/// telescoping for FV; the only one implemented so far); ghost cells via the
/// existing [`crate::boundary::BoundaryCondition`] system (a real
/// integration, scheduled later this sprint, after `#49`); and FD-style
/// one-sided truncation at the boundary (breaks FV's conservation property).
/// Rather than picking one silently, `FluxBoundary` makes the choice an
/// explicit constructor parameter on every operator that needs it.
/// `#[non_exhaustive]` (DD-022) signals this is an extension point, not a
/// closed set — only [`FluxBoundary::Periodic`] exists today.
// ── check_cfl ─────────────────────────────────────────────────────────────────
/// Checks the explicit advective CFL stability condition `|v|·dt/dx ≤ 1`.
///
/// Shared by `operators::fv` and `operators::weno` — both are explicit
/// advective schemes subject to the same necessary stability condition
/// (documented for Lax–Wendroff-type schemes). Covers the *advective*
/// condition only; a diffusive term's own stability limit (Fourier number
/// `D·dt/dx² ≤ 0.5`) is a separate concern, not checked here — conceptually
/// the solver's step-size control's responsibility, not a spatial
/// operator's, if ever enforced.
pub
// ── Shared wide-stencil divergence helpers ──────────────────────────────────
//
// Originally introduced in `operators::weno` (#49) for its multi-point
// stencils, relocated here `pub(crate)` once `operators::limiters` (#99)
// turned out to need the exact same boundary-handling logic for its
// 3-point MUSCL stencil — a second concrete consumer, not a speculative
// generalization (same posture as DD-042's own "no anticipation without a
// second concrete need").
/// Periodic index `i + k` wrapped into `[0, n)`, `k` possibly negative.
pub
/// Computes the divergence of a wide-stencil face flux for a periodic
/// domain — analogous to `operators::fv`'s two-point `periodic_divergence`,
/// generalized to a `face_flux` that reads an arbitrary window of `u`
/// rather than just the two immediately adjacent values.
///
/// `face_flux(u, n, i)` evaluates the flux at the face between node `i` and
/// node `(i+1) % n`.
pub
/// Computes the one-sided (decentered) divergence for a non-periodic domain
/// — [`FluxBoundary::Truncation`], wide-stencil generalization of
/// `operators::fv`'s `truncated_divergence`.
///
/// `face_flux(u, n, k)` reads offsets in `[-margin_left, margin_right]`
/// relative to `k` (asymmetric for upwind-biased stencils — e.g. WENO3's
/// left-biased reconstruction reads `{k−1, k, k+1}`, its right-biased one
/// reads `{k, k+1, k+2}`; the caller computes `margin_left`/`margin_right`
/// from the reconstruction actually selected, not a fixed value, so the
/// boundary zone is no wider than the stencil in use actually requires). A
/// cell `i`'s divergence reads `face_flux(i)` (right face) and
/// `face_flux(i−1)` (left face); the safe (non-wrapping) range for both is
/// `[margin_left+1, n−1−margin_right]`. Cells outside that range — lacking a
/// full stencil on at least one side — are assigned the same value already
/// computed for the nearest safe cell, the same posture as
/// `operators::fv::truncated_divergence` and `operators::fd`'s boundary
/// stencils. The two boundary zones are generally of different sizes
/// (`margin_left+1` cells on the left, `margin_right` cells on the right) —
/// an inherent consequence of `face(i)` and `face(i−1)` both needing to be
/// safe for cell `i`, not an asymmetry bug.
pub
/// Builds a ghost-padded copy of `u` for [`FluxBoundary::GhostCell`] —
/// `margin_left` ghost values prepended, `u` in the middle, `margin_right`
/// ghost values appended, so that every real cell's stencil (however wide)
/// finds all its neighbors in-bounds with plain indexing — no wraparound.
///
/// Each ghost value comes from [`BoundaryCondition::ghost_value`] at the
/// depth matching its distance from the domain edge, paired with the real
/// interior value at the *symmetric* depth (method of images — DD-042,
/// amendment 1): depth `k` pairs with interior index `k−1` from that edge.
/// Every layer is derived directly from a real interior value, never from a
/// previously computed ghost layer, so a Robin condition's exactness does
/// not erode with depth.
pub
/// Computes the divergence for [`FluxBoundary::GhostCell`] by delegating to
/// `face_flux` over a [`ghost_padded_field`] — every real cell's stencil,
/// including the two boundary ones, reads only in-bounds data, so no
/// separate boundary-cell code path is needed here (unlike
/// [`truncated_wide_divergence`]): the padding does the work.
pub