Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf6f1d122 |
@@ -112,21 +112,18 @@ from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
cfg = LBMConfig(nx=32, ny=24, viscosity=.1)
|
||||
body = SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.0), radius=2.15,
|
||||
surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(cfg, context=ctx, refinements=[(8, 6, 24, 18)],
|
||||
level_blocks=[None, (16, 20)],
|
||||
level_objects=[(), (body,)]) as run:
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
|
||||
fine_objects=[body], fine_block=(16, 20)) as run:
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
# (9, ny, nx) postcollision populations per level; non-leaf cells are discarded.
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].fine_shape)))
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.patch.fine_shape)))
|
||||
run.step(128) # coarse steps; fine level takes two per step
|
||||
coarse_g, fine_g = run.download() # NaN on covered, ghost and solid cells
|
||||
```
|
||||
|
||||
Scope and limits:
|
||||
- The examples below use D2Q9, SRT, FP32, double buffer, unshifted, no LES or
|
||||
forcing. Fixed 2:1 hierarchies use level lists of arbitrary depth; two- and
|
||||
three-level examples below use that same constructor.
|
||||
- D2Q9, SRT, FP32, double buffer, unshifted, no LES or forcing; fixed 2:1 patch(es), two levels (or one nested third).
|
||||
- `coarse_boundary="periodic"` (default) or `"slot_reservoir"` (bottom slot, top
|
||||
reservoir; patch needs `y0 >= 4`, `y1 <= ny-4`). Periodic mode ignores the wall/slot config fields.
|
||||
- Object centers/radii use the coarse **cell-edge** frame (cell `I` spans `[I, I+1)`),
|
||||
@@ -134,51 +131,46 @@ Scope and limits:
|
||||
- Bodies must be stationary circles whose whole boundary stencil lies in the fine
|
||||
active region, clear of the coarse/fine transfer cells; violations are rejected
|
||||
before any GPU allocation.
|
||||
- `level_blocks=[None, (by, bx)]` stores the fine level in fixed-size blocks; `by`/`bx` must
|
||||
- `fine_block=(by, bx)` stores the fine level in fixed-size blocks; `by`/`bx` must
|
||||
divide the WHOLE fine allocation, i.e. `2*(y1-y0)+8` by `2*(x1-x0)+8` including the
|
||||
four-cell ghost band. It gives results identical to dense storage and is
|
||||
infrastructure for future block AMR, not a speedup.
|
||||
- Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as
|
||||
`refinements[0]`, with both levels blocked (`level_blocks`). Transfers are built once
|
||||
from their union; adjacent, overlapping and concave unions are accepted when
|
||||
their actual dependencies close. There is no fixed rectangle-gap rule.
|
||||
The fine level is then one level-wide grid `(2*ny, 2*nx)`:
|
||||
`run.hierarchy.pairs[0].fine_shape` gives it, `initialize`/`download` use it, and only the
|
||||
grid-aligned tiles intersecting the union plus its four-cell ghost band are stored.
|
||||
Block sizes must divide the level-wide grid; rectangle edges need not align
|
||||
with tiles. Order of rectangles does not matter. L1 bodies/Jets (`level_objects[1]`/`level_jets[1]`) work
|
||||
as for one patch: each circle (same coarse cell-edge frame) must lie wholly
|
||||
inside the union's active region with
|
||||
- Several separated patches: pass a sequence of `(x0, y0, x1, y1)`
|
||||
rectangles as `bounds`, at least 4 coarse cells apart, with both `fine_block`
|
||||
and `base_block`. The fine level is then one level-wide grid `(2*ny, 2*nx)`:
|
||||
`run.patch.fine_shape` gives it, `initialize`/`download` use it, and only the
|
||||
patch windows (active region plus 4-cell ghost band, origin `(2*y0-4, 2*x0-4)`)
|
||||
are stored; each window must align with `fine_block`. Order of rectangles does
|
||||
not matter. `fine_objects`/`jets` work as for one patch: each circle (same
|
||||
coarse cell-edge frame) must lie wholly inside one patch's active region with
|
||||
its boundary stencil clear of transfer cells; a jet may not select links of two
|
||||
different bodies, even across regions. Wall-touching patches remain unsupported.
|
||||
For nesting, add further `refinements` entries as shown below.
|
||||
different bodies, even across patches. No wall-touching patches or nesting yet.
|
||||
|
||||
```python
|
||||
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||
cfg, context=ctx, refinements=[((4, 4, 12, 12), (16, 4, 24, 12))],
|
||||
level_blocks=[(4, 4), (4, 4)]) as run:
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].fine_shape))) # (9, 32, 64)
|
||||
cfg, ((4, 4, 12, 12), (16, 4, 24, 12)), context=ctx,
|
||||
fine_block=(4, 4), base_block=(4, 4)) as run:
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
|
||||
run.step(32)
|
||||
coarse_g, fine_g = run.download() # coarse: NaN on every covered cell;
|
||||
# fine: NaN on every non-ACTIVE cell
|
||||
```
|
||||
- Three-level nested windows: pass two scalar rectangles in `refinements` —
|
||||
L1 in L0 cells, L2 in **L1 window cells** (the parent's fine grid including its
|
||||
4-cell ghost band) — plus all three `level_blocks` entries. L2 must sit inside
|
||||
L1's ACTIVE cells, away from L1's ghost band and transfer footprint (checked
|
||||
before any GPU allocation). `initialize` and `download` take/return three
|
||||
arrays; each level is NaN on its non-ACTIVE (and child-covered) cells. One
|
||||
coarse step = 2 L1 steps = 4 L2 steps. One L1 window only in this scalar
|
||||
chain. Optional stationary circles and Jets live on L2 via `level_objects[2]`
|
||||
and `level_jets[2]`; L1 bodies/Jets retain their L1 meaning and must be empty
|
||||
in this mode. All object and Jet geometry uses the **L0 edge frame**, not the
|
||||
child bounds' L1 window frame. L2 has dx=dt=0.25, so prescribed lattice
|
||||
velocity is unchanged. Each whole body and its BFL stencil must be inside L2
|
||||
ACTIVE cells, outside the transfer footprint. A Jet may select one physical
|
||||
body across multiple blocks, never two bodies.
|
||||
- Three levels (one nested chain): add `child=(x0, y0, x1, y1)` in
|
||||
**L1 window cells** (the patch's fine grid including its 4-cell ghost band) and
|
||||
`child_block`, with `fine_block` and `base_block`. L2 must sit inside L1's
|
||||
ACTIVE cells, away from L1's ghost band and transfer footprint (checked before
|
||||
any GPU allocation). `initialize` and `download` take/return three arrays; each
|
||||
level is NaN on its non-ACTIVE (and child-covered) cells. One coarse step = 2 L1
|
||||
steps = 4 L2 steps. One L1 patch only. Optional stationary circles and Jets
|
||||
live on L2 via `child_objects` and `child_jets`; `fine_objects`/`jets` retain
|
||||
their L1 meaning and must be empty in this mode. All object and Jet geometry
|
||||
uses the **L0 edge frame**, not the child bounds' L1 window frame. L2 has
|
||||
dx=dt=0.25, so prescribed lattice velocity is unchanged. Each whole body and
|
||||
its BFL stencil must be inside L2 ACTIVE cells, outside the transfer footprint.
|
||||
A Jet may select one physical body across multiple blocks, never two bodies.
|
||||
|
||||
```python
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
@@ -190,40 +182,24 @@ Scope and limits:
|
||||
jet = Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=.002)
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||
cfg, context=ctx, refinements=[(4, 4, 12, 12), (8, 8, 16, 16)],
|
||||
level_blocks=[(4, 4)] * 3, level_objects=[(), (), (body,)],
|
||||
level_jets=[(), (), (jet,)]) as run:
|
||||
shapes = (run.hierarchy.pairs[0].coarse_shape, run.hierarchy.pairs[0].fine_shape, run.hierarchy.pairs[1].fine_shape)
|
||||
cfg, (4, 4, 12, 12), context=ctx, fine_block=(4, 4), base_block=(4, 4),
|
||||
child=(8, 8, 16, 16), child_block=(4, 4),
|
||||
child_objects=[body], child_jets=[jet]) as run:
|
||||
shapes = (run.patch.coarse_shape, run.patch.fine_shape, run.child_patch.fine_shape)
|
||||
run.initialize(*(np.broadcast_to(rest, (9, *s)) for s in shapes)) # 16², 24², 24²
|
||||
run.step(4)
|
||||
run.set_jet_amplitude(0, -.002, level=2) # explicit level selects the driven level
|
||||
run.set_jet_amplitude(0, -.002, level=2) # default level=1 still addresses L1
|
||||
run.step(4)
|
||||
coarse_g, l1_g, l2_g = run.download() # solids/covered/nonleaves are NaN
|
||||
```
|
||||
- Level lists (the only constructor): `refinements[l]` describes level `l+1`
|
||||
in allocated level-`l` integer cells; use a rectangle list for a union, or a
|
||||
scalar rectangle for a single window (a scalar child requires a scalar
|
||||
parent). `level_blocks`, `level_objects` and `level_jets` each have one entry
|
||||
per level, including L0. Three or more levels require all-blocked storage and
|
||||
bodies/Jets only on the finest level. Every adjacent pair must preserve
|
||||
inherited ACTIVE coverage, valid donors/receivers, surviving pulls and the
|
||||
upstream transfer footprint. Level `l` takes `2**l` steps per base interval.
|
||||
This describes implemented topology and scheduling, not accuracy or
|
||||
convergence qualification.
|
||||
- Blocked body levels can enable `observe_raw_exchange=True` and call
|
||||
`read_raw_exchange(body_id, level=...)` after a completed base interval.
|
||||
It reduces unique cut-link observations into wall/aperture/total force,
|
||||
torque and mass exchange in base-lattice units per unit depth, for the last
|
||||
base interval (default) or `window="last_substep"`.
|
||||
`observe_window_exchange=True`, alongside `observe_raw_exchange=True`, enables
|
||||
`start_exchange_window()` / `read_exchange_window(body_id, level=...)` across
|
||||
completed base intervals. These are raw boundary-reaction diagnostics, not a general
|
||||
stress or nozzle-load accuracy claim; dense body layouts are unsupported.
|
||||
- Not available: checkpoints, moving bodies, L1 bodies/Jets in a three-level
|
||||
chain, automatic dynamic refinement, `Simulation` integration. Qualification
|
||||
depends on the tested configuration; see [the validation scope](docs/validation/2d_scope.md).
|
||||
CPU topology/schedule tests alone do not establish GPU numerical correctness,
|
||||
general convergence or long-time stability.
|
||||
- Not available: per-link surface exchange/force readout in blocked mode,
|
||||
checkpoints, moving bodies, L1 bodies/Jets in a three-level chain,
|
||||
several patches per level with nesting, dynamic refinement, `Simulation`
|
||||
integration. Validation so far is storage/operator equivalence and short runs,
|
||||
not accuracy, convergence or long-time stability. The new L2-body GPU gates
|
||||
(layouts, masks, amplitude reversal, legacy equivalence) and independent
|
||||
every-substep numerical replay remain required acceptance checks; CPU tests
|
||||
alone do not establish GPU numerical correctness.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -394,12 +370,6 @@ before `run()`, the force will be reset to zero. For the common usage pattern
|
||||
| `sim.load_checkpoint(path)` | Restore from HDF5 (config must match) |
|
||||
| `sim.snapshot()` / `sim.restore()` | In-memory field snapshot |
|
||||
|
||||
In-memory snapshots save populations only. Restore keeps the current clock,
|
||||
controls, observations, geometry and flags; use it only while geometry is
|
||||
unchanged. This is not a restart or an FSI rollback. In particular, do not
|
||||
restore a snapshot taken before a translating circle changed position.
|
||||
This geometry restriction is a caller precondition, not a runtime check.
|
||||
|
||||
#### Low-level access
|
||||
|
||||
| Attribute | Description |
|
||||
@@ -693,17 +663,6 @@ for step in range(steps):
|
||||
streak.render("streakline.png")
|
||||
```
|
||||
|
||||
## Validation campaigns
|
||||
|
||||
- [Periodic Taylor–Green AMR](docs/validation/taylor_green.md): analytic periodic-decay pilot and frozen baseline-relative assessment.
|
||||
- [Matched refined DFG](docs/validation/refined_dfg.md): frozen SRT uniform/refined comparison, requiring channel GPU gates before execution.
|
||||
- [DFG 2D-2 Re100](docs/validation/dfg_re100.md): frozen periodicity and official-reference comparisons, with a declared noncanonical outlet.
|
||||
- [Uniform LES shear decay](docs/validation/uniform_les_flow.md): independent continuum-oracle flow diagnostic, baseline and isolated corrected source variants.
|
||||
- [Smagorinsky helper comparison](docs/validation/les_helpers.md): pinned baseline versus an output-local two-coefficient candidate, without production edits.
|
||||
- [Jet-circle control volume](docs/validation/jet_circle_cv.md): bounded AMR versus uniform-grid conservation diagnostic.
|
||||
|
||||
The runners are in `tests/validation`, their CPU tests in `tests/unit`, and compact reference/source guards in `tests/validation/data`. Current Re100/Jet source guards explicitly pin the reviewed AMR-channel experiment; historical d79 evidence remains tied to its original checkpoint commits. New runs record actual Git HEAD separately from source origin. These campaigns preserve pinned production code; successful CPU checks alone do not establish GPU or physical validation. Keep generated results outside tracked files.
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Compatibility and API changes
|
||||
|
||||
Changes that can make a previously accepted setup or call behave differently.
|
||||
Each entry names the commit, the affected entry points and the observable result.
|
||||
|
||||
## Redundant periodic stepper alias removed
|
||||
|
||||
`PeriodicPatchStepper` is no longer exported from `lbm.refinement_stepper`.
|
||||
Use `FixedPatchStepper` with the same arguments, including explicit
|
||||
`level_blocks`. The old name was an alias for that exact class; periodic
|
||||
boundaries remain the default. Existing imports of the old name must be updated.
|
||||
|
||||
## 87c3a31: cut-link ownership is validated for every body
|
||||
|
||||
`validate_cut_link_ownership` (`body/preprocess/flag_overlay.py`) now runs for
|
||||
passive bodies as well as Jet/aperture bodies. Every cut link must have an
|
||||
active fluid receiver (FLUID set, SOLID clear), and each `(receiver, direction)`
|
||||
population may have only one writer.
|
||||
|
||||
Rejected with `ValueError`, before flags, populations or device buffers change:
|
||||
|
||||
1. Overlapping or touching passive solids whose cut links write the same
|
||||
population ("Boundary requires unique cut-link writers").
|
||||
2. A body whose cut-link receiver is a non-fluid cell, e.g. a channel wall row
|
||||
or the inlet/outlet column ("Cut links must have active fluid receivers").
|
||||
|
||||
Both were previously accepted silently for passive bodies; the interpolated
|
||||
boundary then had undefined results. The rules apply to the cut links of all
|
||||
bodies together, so a passive body overlapping a Jet body is rejected as well.
|
||||
The check runs in `Simulation.initialize`, runtime body sync (`sync_bodies`),
|
||||
checkpoint save and checkpoint load (`common/checkpoint.py`). Load validates
|
||||
before restoring anything, so a checkpoint whose saved geometry violates either
|
||||
rule no longer restores; rebuild the geometry with separated bodies.
|
||||
|
||||
## 87c3a31: `Simulation.read_surface_exchange` accepts Jet-driven bodies
|
||||
|
||||
`read_surface_exchange(id)` previously required `SurfaceActuation` on the body.
|
||||
It now also accepts bodies that own Jet aperture links, including Jets with zero
|
||||
or negative amplitude (the aperture test is geometric). Bodies with neither
|
||||
`SurfaceActuation` nor aperture links still raise
|
||||
`ValueError("Body has no surface actuation")`. The value is unchanged: the latest
|
||||
step's wall/aperture exchange, not total nozzle force.
|
||||
|
||||
## 7afb5ad: `Simulation` fails closed after a Jet amplitude device-write failure
|
||||
|
||||
`Simulation.set_jet_amplitude` previously only documented that a device error
|
||||
leaves the per-link velocity column undefined; the instance stayed usable.
|
||||
Now any error after input validation (internal stream creation or the upload)
|
||||
marks the Simulation failed. Every later device-touching public method,
|
||||
including the `stream` property (guard added in the follow-up repair commit on
|
||||
`claude/jet-update-consistency`), raises
|
||||
`RuntimeError("Simulation failed (...); close() and recreate it")`. The host
|
||||
amplitude keeps its previous value. `close()` still works and stays retryable;
|
||||
cached host readers and host staging calls are not blocked. Invalid input
|
||||
(`ValueError`/`IndexError`) is unchanged: it raises before any change and the
|
||||
instance stays usable. `FixedPatchStepper.set_jet_amplitude` already failed the
|
||||
instance (status `failed`) and is unchanged.
|
||||
@@ -1,86 +0,0 @@
|
||||
# Continuous VIV videos
|
||||
|
||||
Research-only, two separate settings, each one uninterrupted OFF -> ON -> OFF
|
||||
fluid/oscillator trajectory. No production changes, topology switching, moving
|
||||
checkpoint feature, retraining, force filtering, or parameter search.
|
||||
|
||||
The user selected separate videos because runtime topology changes are unsupported
|
||||
with translating circles. This is deliberately different from the old Legacy
|
||||
multi-setting video, which rebuilt solvers and transferred full state.
|
||||
|
||||
## Frozen physical and control contract
|
||||
|
||||
- Source baseline `73d3f6214e0d28ced4744665aa3bb05522aa2fa8`; D40, U=.005,
|
||||
nu=.004, MRT/FP32/unshifted double-buffer. Same parabolic regularized inlet,
|
||||
NEQ outlet and bounce-back walls as the retained m*=4 pair.
|
||||
- Common transverse assembly: m*=4 (all three disks), fn D/U=.2, zeta=0.
|
||||
Structure uses previous native-step sum CME_y+Wen_y. No policy-force averaging
|
||||
is applied to this RHS. Same +/-D travel, speed, finite-state and no-donor stops.
|
||||
- Each phase: Karman400 complete SI of3200, steady320 SI of4000 native steps
|
||||
=160 D/U. Steady's SI follows the actual old video1000-step interval under
|
||||
diffusive D40 scaling, rather than borrowing PPO's800-step interval. Total480 D/U.
|
||||
No extension, retuning or replacement if steady motion/control is weak.
|
||||
- Steady: canonical new-reproduce constant surface velocities
|
||||
(0,-5.1,+5.1) U. This is NOT a PPO transfer. No model/normalizer is loaded.
|
||||
Existing 2,048,000-step at-rest warmup precedes the free response.
|
||||
- Karman: actual pinball Re_D50, historically named `karman_re100`, frozen
|
||||
model `148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d`
|
||||
and norm `534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0`.
|
||||
Use the verified static release once, at video start (clock4576000).
|
||||
- Both switches occur on SI boundaries. The oscillator y/v, previous RHS,
|
||||
both DDF buffers, moving geometry and solver clock are never reset. OFF still
|
||||
gathers sensor/CME observations. First PPO action uses the immediately previous
|
||||
complete OFF interval, unlike an independent arm's initial all-zero observation.
|
||||
Deterministic feed-forward PPO has no recurrent state. EMA resets to zero each
|
||||
SI, as in training/reproduction; original video's SplitAdvance also reset the
|
||||
constant-steady actuation every interval. Physical OFF is exactly zero wall speed.
|
||||
There is no extra transition blend. The first1 D/U after either switch is
|
||||
explicitly labeled a switch transient. ON from an already moving state differs
|
||||
from the previous independent pair; +/-D stops are retained without retuning.
|
||||
A visual ON label means enabled control,
|
||||
not a proven suppression/cloaking result.
|
||||
|
||||
## Video provenance and presentation
|
||||
|
||||
Read-only source: DynamisLab `src/drl_pinball/reproduce/continuous_video.py`, SHA256
|
||||
`3bd9c777171c9e8932c78298accbb6f61a8323b0eed5938e0f56e167b3df0743`.
|
||||
Approved original MP4 SHA256
|
||||
`578d2cf5113429006b6919bbb8ef8ff6ce65ebcd0c056ac478f8ce2515e4cbab`.
|
||||
The actual new-reproduce conversation confirms fixed-position plain-language
|
||||
labels, white bars, OFF/ON toggle, rotation sectors, and 1/2/5/10/5/2/1 gears.
|
||||
|
||||
Only the display idiom/gear schedule are reused. Legacy population decoding,
|
||||
physics, topology handoff and policy runner are not imported. Modern macroscopic
|
||||
fields feed centered vorticity with all stencil nodes fluid. Fixed color limits
|
||||
[-3,+3], 21 RdBu_r bins; per-frame clipping fraction retained. Box downsampling
|
||||
is display-only. Both main field and enlarged inset have fixed lab coordinates;
|
||||
the inset shows the equilibrium line. Actual moving centers and applied surface
|
||||
speeds drive the body overlays. y/D trace has fixed +/-1 limits. No motion gain.
|
||||
|
||||
1280x800,30fps; first80 D/U of each phase uses the original display gear ramps,
|
||||
last80 D/U uses normal cadence (800 native steps/frame =.1D/U). Every native
|
||||
step and every SI is still executed. Streaming ffmpeg avoids stored PNG sequences.
|
||||
Keep raw scalar histories, SI observations/actions, per-frame time/motion records,
|
||||
selected keyframes, source/input hashes and terminal status. These are new
|
||||
continuous runs, not an exact replay of the old independent paired histories.
|
||||
|
||||
## Gates and limits
|
||||
|
||||
CPU: switch-state/clock continuity, first ON observation, exactly zero OFF action,
|
||||
SI timing despite sampling, physical scaling, schedule monotonicity, vorticity
|
||||
sign/mask, fixed-coordinate mapping. Real smoke:32 warmup +3x32 response steps
|
||||
on D40 (smoke SI32), all switches/encoder; not a physical comparison. Grok reviews
|
||||
the exact research diff before full runs. Capture no-donor, finite state, native
|
||||
RHS and all stop outcomes. Report per-phase late-half motion statistics and
|
||||
raw-force limits; appearance and successful duration are not physics qualification.
|
||||
|
||||
Example (outputs must be new, outside checkout; select a safe GPU):
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src python research/continuous_viv/run.py --scene steady --out /tmp/celeris-results-continuous-viv/steady
|
||||
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src python research/continuous_viv/run.py --scene karman --model /tmp/celeris-karman-viv-assets/model.zip --norm /tmp/celeris-karman-viv-assets/norm.json --release-dir /tmp/celeris-results-karman-viv-516975e/pair --out /tmp/celeris-results-continuous-viv/karman
|
||||
```
|
||||
|
||||
Append `--smoke` and use separate output directories for the bounded gates.
|
||||
The Karman smoke does not read/install the physical release. Source and review
|
||||
SHA, interpreter/version and GPU resource readings belong in the execution receipt.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Selective LES normalization integration on dev8403956
|
||||
|
||||
This change reuses the reviewed correction from 5465832 without merging its
|
||||
branch history. The only production change is the two closed-form coefficients
|
||||
18 -> 18 sqrt(2) and their explanatory comments in turbulence_smag.cuh. Its SHA256
|
||||
is a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa.
|
||||
Defaults, iterative/deviatoric/capped closure, collision dispatch, AMR guards,
|
||||
and all other production source files are unchanged.
|
||||
|
||||
## Separate source contract; unchanged experiment
|
||||
|
||||
The single existing flow runner now accepts `--experiment dev8403956`.
|
||||
Its separate source_pins_dev8403956.json pins all 101 production files from the
|
||||
committed 8403956 baseline; corrected mode permits only the exact reviewed
|
||||
header substitution. Results record the actual checkout commit, import path,
|
||||
experiment name and manifest SHA256. No arbitrary manifest-path override exists.
|
||||
|
||||
Omitting the option still selects the original historical source manifest.
|
||||
That manifest, the original paired receipts, scalar preparation manifests and
|
||||
independent continuum oracle are unchanged. Historical baseline and corrected
|
||||
receipts remain evidence for their own exact sources, not new dev receipts.
|
||||
Historical guards intentionally reject this different dev source tree. Eight
|
||||
production files already differed between the historical source snapshot and
|
||||
8403956; neither the old manifest nor its acceptance is silently regenerated.
|
||||
|
||||
The physical experiment and every numerical acceptance threshold are unchanged:
|
||||
8 x 34 nodes, halfway y walls, periodic x, unforced D2Q9/SRT, shifted FP32,
|
||||
double-buffer streaming, Cs=0/.16/1, checkpoints 519/1038/2075. Each case has one
|
||||
actual advancing warm-up followed by complete initial-state restoration before
|
||||
READY. Total launches are 6,225 measured plus three warm-ups. Cs=1 remains an
|
||||
amplified diagnostic, not a physical coefficient recommendation.
|
||||
|
||||
## Host checks
|
||||
|
||||
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_smagorinsky_normalization.py' -v
|
||||
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_les_flow_*.py' -v
|
||||
|
||||
The direct-current-header test is copied unchanged from reviewed 5465832:
|
||||
120 tensor parameter cases, Cs=0/equilibrium and omega guards, converged
|
||||
trace-free iterative comparison, and mutation detection in both lattice
|
||||
algebras. This is scalar D2/D3 algebra coverage, not 3D-flow qualification.
|
||||
Flow tests retain analytic/reference checks and add current named-source,
|
||||
reverse-patched baseline, variant rejection, unrelated-source tampering and
|
||||
immutable historical-default checks. The temporary baseline is constructed
|
||||
only inside a test directory; production is never reverse-patched.
|
||||
|
||||
## Pending final-commit GPU gate
|
||||
|
||||
After independent review and explicit executor authorization, run from the
|
||||
exact committed candidate checkout, selecting the authorized physical GPU:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --root "$PWD" --experiment dev8403956 --variant corrected --run-gpu --device 0 --output <new-evidence-directory>/corrected.json
|
||||
|
||||
Retain all source hashes, exact commit/import/device identity, manifest hash,
|
||||
three READY/reset records, complete result JSON, stdout/stderr and actual exit.
|
||||
Require all frozen gates, DONE passed=true and exit 0. Compare with historical
|
||||
corrected evidence without relabeling it as current evidence. The old-vs-new
|
||||
normalization discrimination already exists; no new baseline GPU run is needed
|
||||
unless review finds a changed exercised uniform solver path.
|
||||
|
||||
No GPU result is claimed by this integration preparation. This does not expand
|
||||
LES qualification to MRT, forcing, AMR LES or three-dimensional turbulent flows.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Experimental projected G explosion
|
||||
|
||||
`FixedPatchStepper(..., explosion="projected_linear")` changes only the existing
|
||||
G-to-G explosion reconstruction. The omitted/default `"constant"` selector retains
|
||||
the same kernel, launch arguments and device allocations. Projected mode adds two
|
||||
compact tables per adjacent pair; recursion, collision, BFL, restriction, buffer
|
||||
ownership and synchronized publication are unchanged. `remap_finest` preserves it.
|
||||
|
||||
Runtime eligibility is periodic-root, fully blocked D2Q9/SRT/FP32, body/Jet-free,
|
||||
without LES; the fixed-patch backend has no forcing interface. Shifted and unshifted
|
||||
storage use the same reconstruction. One generic builder handles scalar windows,
|
||||
unique union rows and all adjacent hierarchy pairs without depth/rectangle caps.
|
||||
Donors are original explosion donors intersected with owned, uncovered ACTIVE
|
||||
cells. An axis uses centered differences only if both neighbors are legal;
|
||||
otherwise both addresses repeat the center. Nested layout periodicity is explicit.
|
||||
No covered cells, holes, inactive cells or stale halo copies enter the stencil.
|
||||
|
||||
The 33-line kernel is unchanged from our `519473a` experiment and retains its
|
||||
mathematical attribution. Host tests cover union decomposition, concavity, narrow
|
||||
gaps, grazing packet paths, offsets, sparse/nested owner addresses, three/four-level
|
||||
source timing, unchanged constant dispatch, remapping and shifted/unshifted actual
|
||||
kernel arithmetic compiled as host C++. These are not CUDA or flow validation.
|
||||
|
||||
## Reviewed bounded evidence
|
||||
|
||||
The `1aa8110` CUDA gate, with production exactly `a135252`, passed exact-dev
|
||||
omitted/default and explicit-constant equivalence, per-population reconstruction,
|
||||
scalar/union/four-level scheduling, alternate layouts, and changed remapping in
|
||||
both storage modes. Operator maximum error was 2.98023224e-8; the eight-step
|
||||
concave replay maximum was 1.58037614e-7. Whole-step mass/momentum remain
|
||||
finite diagnostics, not an FP32 conservation certificate. Reviewed evidence:
|
||||
`7aa66ff0542175092753ea099f40b903ece0527e`.
|
||||
|
||||
The fresh four-case `2cf9853` N128/256 Taylor–Green repeat retained all `11901a7`
|
||||
settings and gates. All 20 main scalar rows and 260 early-pressure samples matched
|
||||
the older receipts exactly; this is not field-array equality. Projected worst
|
||||
velocity was 0.001995299260844 versus 0.002 (only 0.235% margin), pressure
|
||||
0.012157174904932 and energy 0.001184362645162. Constant velocity
|
||||
0.004418076615223 failed its gate, so the aggregate verdict remains false.
|
||||
The projected early sampled pressure peak was 0.021621453573334; it remains a
|
||||
descriptive sampled maximum. Reviewed evidence: `90849a55cad4a0da9b72747da7bfe6eb75962a44`.
|
||||
|
||||
The old N64 projected failures remain relevant. These results support only this
|
||||
bounded plumbing gate and short periodic Re100 smooth-decay experiment. They do
|
||||
not establish union/multilevel physics accuracy, convergence, long-time stability,
|
||||
body/Jet, boundary, forcing, MRT or LES suitability. Constant remains the default.
|
||||
|
||||
## Separate LES source profile
|
||||
|
||||
The two older LES source manifests and the historical default are unchanged and
|
||||
correctly reject this different source tree. The existing LES runner now offers
|
||||
`--experiment a135252`, pinning all 101 actual committed corrected source files.
|
||||
Its explicit baseline-header override describes only a synthetic isolated reverse
|
||||
control, never the committed a135252 header. The unchanged corrected-only uniform
|
||||
LES regression (Cs=0/.16/1) is the remaining integration gate; no LES result is
|
||||
inherited from the projected evidence. The separate historical jet-CV `channel.py`
|
||||
pin mismatch predates this port and is not addressed here.
|
||||
@@ -1,34 +0,0 @@
|
||||
# MRT reuse in blocked/refined stepping (candidate)
|
||||
|
||||
Base `9805383`. Opt-in by `LBMConfig(collision="MRT")`; SRT remains the default and its
|
||||
kernel statements, including the shifted retained-delta/direct-momentum path, are unchanged
|
||||
under the alternate compile branch.
|
||||
|
||||
## What changes
|
||||
- `blocks.cu` (`BlockStep`), `step/block_channel.cu` (`BlockChannelStep`) and `refinement.cu`
|
||||
(`patch_collide`) gain a compile-time `COLLISION_MODEL == 2` branch that repeats the uniform
|
||||
`collide_dispatch` sequence with zero forcing: `compute_rho_u`, omega clamp to
|
||||
`[OMEGA_COLLISION_MIN, OMEGA_COLLISION_MAX]`, `collide_mrt_no_force`.
|
||||
- Kernel and host guards accept SRT or MRT only. TRT, LES, non-FP32 storage, non-double-buffer
|
||||
streaming, and shifted dense/mixed refinement remain rejected.
|
||||
- Transfers, subcycling, BFL, raw exchange ledgers and channel closures are untouched. The
|
||||
existing pressure-NEQ outlet selects its MRT branch (unknowns-only NEQ extrapolation) from
|
||||
`COLLISION_MODEL`, so MRT channel parity must use uniform MRT as reference.
|
||||
|
||||
## Contracts (existing operator, not redefined here)
|
||||
- Shifted storage uses the existing MRT representation (equilibrium moments from `rho - 1`),
|
||||
not the SRT retained-delta/direct-momentum precision. Mass and momentum are conserved
|
||||
moments (relaxation rate 0).
|
||||
- Kinetic rates are the operator's fixed `s_e = s_eps = s_q = 1.2` on every level; the shear
|
||||
rate is the existing per-level omega. Under acoustic 2:1 scaling the physical bulk viscosity
|
||||
therefore halves on each finer level. No rate rescaling is applied.
|
||||
- No LES, no external forcing (`d_params` force fields stay zero).
|
||||
- Shifted Jets stay SRT-only: `body/jets.py` keeps its existing guard. This extension does not
|
||||
provide shifted MRT Jets.
|
||||
- `PeriodicBlockStepper` (uniform blocked stepper) remained SRT-only; it was removed in 8e1feff and its single-level capability, still SRT-only, is `FixedPatchStepper(refinements=[])`.
|
||||
|
||||
## Evidence status
|
||||
CPU: `tests/unit/test_refinement_mrt.py` (host guards, source contract). GPU:
|
||||
`tests/validation/multigrid/test_refinement_mrt_gpu.py` (uniform-MRT channel parity, AMR MRT
|
||||
smoke) and unchanged SRT GPU tests. Strict parity may be subject to cross-kernel FMA
|
||||
contraction differences already documented for SRT. Not a DFG/benchmark validation.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Blocked AMR root channel increment
|
||||
|
||||
Opt in with `FixedPatchStepper(..., coarse_boundary="channel", refinements=[bounds], level_blocks=[(by,bx), (fy,fx)])`. Every level must be blocked. The supported boundary is the existing uniform D2Q9/SRT/FP32 double-buffer channel: local Zou-He west inlet, pressure-NEQ east outlet, stationary halfway bounce-back y walls. Uniform and parabolic inlet profiles and shifted/unshifted storage are supported; the existing configuration guards still reject MRT/LES and incompatible storage. This does not change the external DFG MRT reference.
|
||||
|
||||
`step/block_channel.cu` is a small root-only kernel. Fine kernels, transfer kernels and recursive hop scheduling are unchanged. Global coordinates and the global profile height/velocity are separate from block-local storage strides. Corner ownership and collision phases match `OneStep`:
|
||||
|
||||
- y-edge ghosts (including corners): pull, pair swap, no collision
|
||||
- west ghosts at interior y: Zou-He reconstruction then SRT collision
|
||||
- east ghosts at interior y: pressure NEQ from the immutable left input node, no collision
|
||||
- physical fluid beside y walls: same-node opposite-input correction then collision
|
||||
|
||||
The root uses the uniform SRT equilibrium arithmetic, including the existing shifted rho-minus-one path, to preserve matched-root semantics. Fine `BlockStep` retains its existing shifted direct-momentum/retained-density arithmetic. This increment is not a migration of fine arithmetic.
|
||||
|
||||
Root halo topology stays wrapped like the uniform kernel, solely to reproduce its complete evolved ghost state as well as physical populations. It does not make the physical inlet/outlet periodic. Evolved root state includes boundary ghosts, while public fluid-leaf downloads exclude them. Initialization must provide finite positive-density root ghost states. Private raw snapshots preserve them exactly for same-representation continuation; public leaf-only downloads are not restart states. Channel remapping remains explicitly unsupported.
|
||||
|
||||
`channel.py` checks the actual explosion donors and restriction receivers are uncovered bulk fluid and checks actual receiver-local restriction slots against pressure-donor and wall-correction reads. No arbitrary large padding or block-aligned patch restrictions are added. Keep the physical domain dimensions exact: choose legal root block divisors (including full width) instead of padding a benchmark domain. Stationary circles/Jets retain the existing finest-level and transfer-footprint rules.
|
||||
|
||||
## Gates
|
||||
|
||||
- `tests/unit/test_channel_root.py`: host semantics, minimal footprint, overlap rejection, raw-state/public-leaf separation; actual production uniform and blocked kernels under a CPU launch shim, five non-equilibrium steps, all corners, profiles, representations, y seams and a one-cell-wide block outlet donor.
|
||||
- `tests/validation/multigrid/test_channel_root_gpu.py`: exact uniform-versus-blocked full-state GPU parity; AMR finite-state and block-split invariance with/without a stationary circle and velocity Jet; amplitude update and raw snapshot round-trip.
|
||||
- GPU tests require explicit device admission by the caller. CPU-header algebra is not CUDA execution or external physical validation. The current increment does not establish DFG drag/lift/pressure acceptance or a performance speedup; those need matched SRT controls and a separate benchmark run.
|
||||
@@ -1,116 +0,0 @@
|
||||
# src coherence stage (from main 914906e)
|
||||
|
||||
Owner direction 2026-10-28: unified style over historical compatibility, inside `src`.
|
||||
No numerical change is intended. Stages after 5f69eba: 65339ab (one boundary/Jet record,
|
||||
host preparation helpers, pair accessors), c5a3cdb (`block_collide` families), 442979b
|
||||
(exchange metadata on the boundary record). All three received parent/candidate
|
||||
generated-config and host/GPU equality checks. Only the collision slice ran a new PTX
|
||||
matrix (a4fc46d, whose src is identical to c5a3cdb); 65339ab and 442979b reused prior
|
||||
PTX evidence after unchanged CUDA/compiler and generated-input checks. In the collision
|
||||
matrix, only the two channel-root SRT modules changed PTX (omega load/clamp moved);
|
||||
their parent/candidate outputs matched exactly in the exercised cases.
|
||||
|
||||
## Conventions (defined once, in `lbm.hierarchy.Hierarchy`)
|
||||
|
||||
- Level `l = 0 .. depth-1` (0 = root). Pair `hierarchy.pairs[l-1]` builds level `l`.
|
||||
- Array shapes are `(ny, nx)`; rectangles, origins and centers are `(x, y)`.
|
||||
- Coarse/fine name the two members (parent/child) of ANY pair, not absolute L0/L1:
|
||||
`fine_shape`, `fine_origin`, `prepare_fine_boundary`, `blocked_fine_tables`,
|
||||
`blocked_coarse_tables`.
|
||||
- Refined levels (l >= 1) of FixedPatchStepper place bodies/Jets in the root EDGE frame
|
||||
(root cell `i` = `[i, i+1)`). Uniform `Simulation` objects AND the static circles of a
|
||||
single-level (depth 1) FixedPatchStepper use the integer-NODE frame. Nothing converts
|
||||
between them. This concerns input placement only: within the EDGE frame, refined
|
||||
levels still project root-edge coordinates to fine-local cell indices with the
|
||||
half-cell shift (`prepare_fine_boundary`: `(v - o)/dx - .5`; hits back via `fine_hits_to_edge_frame`
|
||||
`origin + dx*(hit + .5)`), using `edge_frame`/`fine_origin` below.
|
||||
- `Hierarchy.edge_frame(l)` -> `((ox, oy), h)`: cell `(i, j)` of level `l`'s allocated
|
||||
array (ghost band and holes included) covers `[o + h*i, o + h*(i+1))`. A scalar
|
||||
FixedPatch2D window carries the half-coarse-cell ghost shift `bounds - HALO/2` in
|
||||
`fine_origin`; a set's level-wide grid has origin 0. Level 1 boundaries are built from
|
||||
the root config (the wall-aware fields live there); deeper ones from the parent frame.
|
||||
- `FixedPatch2D.fine_level_offset` (was `fine_level_origin`) is an array INDEX offset
|
||||
`(y, x)`, not an `(x, y)` origin.
|
||||
|
||||
## FixedPatchStepper migration map
|
||||
|
||||
Historical: left column = names at or after 914906e that no longer exist; right column =
|
||||
the current name.
|
||||
|
||||
| removed / changed | replacement |
|
||||
|---|---|
|
||||
| `patch`, `child_patch` | `hierarchy.pairs[0]`, `hierarchy.pairs[1]` |
|
||||
| `base_layout`, `fine_layout`, `child_layout` (stepper properties) | `layouts[0]`, `layouts[1]`, `layouts[2]` (`FixedPatchSet2D.fine_layout(block)` is unchanged) |
|
||||
| `fine_boundary`, `child_boundary` | `boundaries[1]`, `boundaries[2]` (host geometry; `None` = no bodies/Jets) |
|
||||
| `_jets` | `jets[1]` |
|
||||
| `_boundaries[l]` (dict from L1, level keys), `_level_boundaries` (b4eacf7), `_device_boundaries` (65339ab) | `_boundaries[l]`: the one combined per-level `_BlockedBoundary` record (list from L0, `None` at L0 unless single-level static circles): host geometry `.host`, `.tables`, `.jets`, `.jet_cache`, exchange `.spacing`/`.centers`, device handles |
|
||||
| `_fine_tables`, `_actuated`, `_jet_cache` | `_boundaries[1].tables` / `.actuated` / `.jet_cache` |
|
||||
| `_level_jets` (private) | `_boundaries[l].jets` (public `jets[l]` is derived) |
|
||||
| `_exchange_spacing[l]`, `_exchange_centers[l]` (private) | `_boundaries[l].spacing` (h_l, root units) / `.centers` (root edge frame); only levels with a boundary report exchange |
|
||||
| `_level_blocks` (private) | `_layouts[l].block` |
|
||||
| `_remap_cfg` (private) | `_root_cfg` |
|
||||
| `refinement_tables.blocked_base_tables` (also via `refinement_stepper`) | `refinement_tables.blocked_coarse_tables` (import from `refinement_tables`; the stepper no longer re-exports) |
|
||||
| `_fine_leaf`, `_child_leaf` | `leaf[1]`, `leaf[2]` (copies) |
|
||||
| `_middle_kind` | `hierarchy.kinds[1]` |
|
||||
| `_boundary_hosts` (private) | public `boundaries[l]` = `_boundaries[l].host` (host geometry only; the private record is the combined one) |
|
||||
| `remap_child(rects, child_objects=...)` | `remap_finest(rects, objects=...)` |
|
||||
| `set_jet_amplitude(i, a)` (implicit level 1) | `set_jet_amplitude(i, a, level=l)` (required) |
|
||||
| `read_raw_exchange(b)` / `read_exchange_window(b)` (implicit level 1) | `read_raw_exchange(b, level=l)` / `read_exchange_window(b, level=l)` (required) |
|
||||
| `_level_name(l)` "base/fine/child" messages | `"level l"` |
|
||||
| `refinement.MIN_PATCH_GAP` | deleted (no gap policy exists) |
|
||||
| `params.cuh HaloPlan`, `collide_trt_no_force` | deleted (zero references) |
|
||||
|
||||
Kept: `step_count`, constructor keywords (`refinements`, `level_blocks`, `level_objects`,
|
||||
`level_jets`, `coarse_boundary`, `explosion`), `Simulation.set_jet_amplitude(i, a)` (one level),
|
||||
`RemapTopology.from_config(..., child_objects=, child_jets=)` (internal remap helper; "child" =
|
||||
the replaced level relative to its parent).
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Construction: `_check_levels` (inputs); host preparation, numpy only, before any device
|
||||
work: `_level_layouts`, `_kernel_configs` (per-level kernel configs), `_host_boundaries`,
|
||||
`_prepare_blocked_boundary`, `_fluid_leaves`; `Hierarchy.edge_frame` (geometry frames);
|
||||
then `_planned_bytes()` (memory preflight) and `_build_level` / `_build_pair` /
|
||||
`_upload_boundary` (device). A construction failure closes the partial instance.
|
||||
Observation reads share `_observed_boundary` + `_summarize`.
|
||||
- Pair accessors: `rectangles` (a scalar FixedPatch2D gives `(bounds,)`) and
|
||||
`fine_level_offset` (`(0, 0)` for a set) on both pair types; the types and their
|
||||
storage frames stay distinct.
|
||||
- Per-level state, by lifecycle:
|
||||
- Fixed plan structure (built once in `__init__`, read by `_planned_bytes()` before any
|
||||
allocation, readable after `close()` through the public views): `_layouts`,
|
||||
`_level_cfgs`, `_leaf`, `_state`, and in each `_boundaries[l]` record the host geometry
|
||||
`.host`, link tables, `.jet_cache`, `.spacing`, `.centers`.
|
||||
- Mutable boundary data in the same record: `.jets` and `.tables["surface_velocity"]`,
|
||||
changed only by `set_jet_amplitude` (host prepare -> device write -> commit; an uncertain
|
||||
write fails the instance); device handles (`.flags`, `.device`, `.kernel`, ledgers) are
|
||||
set by `_upload_boundary` and cleared by `close()`.
|
||||
- Runtime: `_levels` (`_Level`) holds compiled modules, kernels, the two population
|
||||
buffers (swapped per step) and transfer/kind handles. It is created only after the
|
||||
memory preflight and cleared on `close()`; `.layout`/`.boundary` reference the plan for
|
||||
`_hop`. Folding the plan into `_Level` would mix both lifecycles in one record.
|
||||
- Blocked kernels: `operators/block_collision.cuh block_collide` is the one collision extension
|
||||
point of `BlockStep` and `BlockChannelStep`. It computes rho/u once and reads no runtime
|
||||
force (`d_params.fx/fy`). Two compile-time families: uniform-equivalent (MRT on any level,
|
||||
SRT on the channel root `BLOCK_ROOT_BC == 2`): clamped omega, `collide_mrt_no_force` or
|
||||
`collide_srt` with its zero Guo term; and level SRT (`BLOCK_ROOT_BC` 0/1): unclamped omega,
|
||||
`collide_srt_no_force`, with the shifted direct-momentum feq under `USE_DDF_SHIFTING`.
|
||||
The uniform path keeps `collide_dispatch` (runtime force, LES, TRT).
|
||||
- `explosion="projected_linear"` stays an optional capability; `"constant"` stays default.
|
||||
|
||||
## Not done / open
|
||||
|
||||
- Two per-level config derivations stay distinct on purpose: `Hierarchy.configs`
|
||||
(`_middle_config`, wall fields reset on every refined level, geometry validation) vs.
|
||||
`FixedPatchStepper._kernel_configs` (reset only on level 1 of a slot_reservoir root;
|
||||
generates each level's config.h). Folding them needs a config.h/PTX diff gate.
|
||||
- No single-`Simulation` facade over FixedPatchStepper: the two own different state models
|
||||
(uniform node frame, body sync, forcing, checkpoint vs. fixed blocked hierarchy, edge
|
||||
frame, remap-as-new-instance). A facade would only forward calls; no concrete need found.
|
||||
- Deferred, each needing its own evidence first: sharing the channel-root cell/donor selector
|
||||
with OneStep (the closure functions are already shared; a shared selector changes uniform
|
||||
signatures and needs a uniform PTX gate); merging FixedPatch2D into FixedPatchSet2D
|
||||
(different origin, ghost band, layout size and addressing; needs a geometry/parity table and
|
||||
would need tiles that are not grid-aligned); one `clamp_omega` helper for uniform and
|
||||
blocked paths (it lives in the hash-pinned LES header `turbulence_smag.cuh`).
|
||||
- Tests, examples and validation callers are migrated separately (pinned runners stay frozen).
|
||||
@@ -1,393 +0,0 @@
|
||||
# Evidence index (draft, R0)
|
||||
|
||||
Active-tree evidence at candidate 0f8fdbc30ea640c764b38ee598d8f4d135c81c85, classified for the release cleanup
|
||||
(thread C0C78UFKGRM 1793021833.593279). Draft for review: nothing is removed by this file.
|
||||
Recovery of any listed file: `git show 0f8fdbc3:<path>` (blob id given; an immutable tag is requested as anchor).
|
||||
|
||||
Classes: K = conclusion kept in the active tree; K-code = tested research code kept; D-snapshot = pre-Gitea
|
||||
patch/diff transport (code landed in git from ed1a6ee on; not yet verified hunk by hunk); P-pin = generated hash inventories, git-status dumps and manifests;
|
||||
R-raw = run outputs, logs, signals, frames, one-shot run scripts and handoff notes. D, P, R leave the active tree in R1.
|
||||
No file under src/ or a test reads any of these paths (git grep at 0f8fdbc3); runners under tests/validation
|
||||
that read research/j* are handled in R2.
|
||||
|
||||
| Class | Files | Bytes |
|
||||
|---|---:|---:|
|
||||
| K | 31 | 196,506 |
|
||||
| K-code | 3 | 24,324 |
|
||||
| D-snapshot | 18 | 338,948 |
|
||||
| P-pin | 50 | 979,177 |
|
||||
| R-raw | 142 | 3,790,554 |
|
||||
|
||||
## (root)
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| BASELINE_JET_AMR.txt | R-raw | 5,244 | 62aa2a66dc05 |
|
||||
|
||||
## output
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| round3_compatibility_summary.md | R-raw | 4,366 | f243318f5591 |
|
||||
|
||||
## research/fine_surface_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 54,507 | 71a4543ead37 |
|
||||
| cpu_device_guard_cmd.txt | R-raw | 241 | 06e0f35d92b3 |
|
||||
| cpu_device_guard_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_device_guard_stdout.txt | R-raw | 98 | 424d584a031c |
|
||||
| cpu_existing_runs.json | R-raw | 650 | 3179536407b7 |
|
||||
| cpu_final_run.json | R-raw | 386 | 523fadb3fec9 |
|
||||
| cpu_final_stdout.txt | R-raw | 99 | 9cfe73da3858 |
|
||||
| cpu_lifecycle_run.json | R-raw | 387 | 5b403b9e2f30 |
|
||||
| cpu_lifecycle_stdout.txt | R-raw | 112 | 3c5c49b285fa |
|
||||
| cpu_maps_stdout.txt | R-raw | 95 | 71dd87f9faf9 |
|
||||
| cpu_preallocation_run.json | R-raw | 337 | fa6e96d98cdd |
|
||||
| cpu_preallocation_stdout.txt | R-raw | 98 | 5d643576f251 |
|
||||
| cpu_shared_kernel_stdout.txt | R-raw | 98 | 8a0c046bb9b1 |
|
||||
| cpu_surface_command.txt | R-raw | 113 | 2a5ff31e8d06 |
|
||||
| cpu_surface_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_surface_projection_command.txt | R-raw | 171 | 70b7f5c62c1a |
|
||||
| cpu_surface_projection_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_surface_projection_stdout.txt | R-raw | 113 | efd37a149cc1 |
|
||||
| cpu_surface_stdout.txt | R-raw | 99 | 9cfe73da3858 |
|
||||
| gpu_preparation_run1.py | R-raw | 14,043 | ee586d035b7a |
|
||||
| gpu_run.json | R-raw | 2,943 | 15b8ba70d528 |
|
||||
| gpu_stdout.txt | R-raw | 5,424 | c5216e0267f3 |
|
||||
| gpu_test_run1.py | R-raw | 15,479 | f30376abc97f |
|
||||
| incremental.diff | D-snapshot | 53,853 | 318d92e7d177 |
|
||||
| manifest.json | P-pin | 8,411 | 8281338416af |
|
||||
| report.txt | K | 8,373 | 6b9e5123e414 |
|
||||
|
||||
## research/fine_surface_joint_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline_00.json | P-pin | 21,302 | 4ae101ebd02b |
|
||||
| baseline_01.json | P-pin | 26,191 | 548cf7b3b494 |
|
||||
| baseline_02.json | P-pin | 22,440 | e0421e7a4052 |
|
||||
| baseline_03.json | P-pin | 8,584 | da99b45f32a4 |
|
||||
| blow_boundary.csv | R-raw | 12,536 | e13363b69457 |
|
||||
| blow_collisions.csv | R-raw | 19,145 | 5d5b395486bb |
|
||||
| blow_ledger.csv | R-raw | 15,977 | b43bbfd0b8d2 |
|
||||
| blow_propagation.csv | R-raw | 9,714 | f06d80d296ca |
|
||||
| blow_signals.npz | R-raw | 221,114 | 528adf75baa2 |
|
||||
| blow_summary.json | R-raw | 4,113 | 32285ac15be3 |
|
||||
| command.txt | R-raw | 232 | 510ecb2925dd |
|
||||
| criteria.txt | K | 7,480 | dd36e76a3be1 |
|
||||
| frozen.json | R-raw | 11,972 | b5a3324e8686 |
|
||||
| gpu_run.json | R-raw | 1,955 | 67ac4785f7e4 |
|
||||
| incremental.diff | D-snapshot | 625 | e03c132d22df |
|
||||
| integrity.json | P-pin | 826 | a8b6e4be88d2 |
|
||||
| manifest.json | P-pin | 4,388 | d2af50260c39 |
|
||||
| regions.json | R-raw | 27,525 | 7bba26904221 |
|
||||
| report.txt | K | 8,207 | d7571e068ad3 |
|
||||
| result.json | R-raw | 11,442 | 9580c2686e70 |
|
||||
| review.txt | K | 2,117 | bbe1d8a83d47 |
|
||||
| runner.py | R-raw | 5,847 | 812973acc000 |
|
||||
| stdout.txt | R-raw | 19,940 | c9218e2e48fc |
|
||||
| suction_boundary.csv | R-raw | 12,505 | b870be05e6c0 |
|
||||
| suction_collisions.csv | R-raw | 19,173 | 2de48c0a84ab |
|
||||
| suction_ledger.csv | R-raw | 16,208 | 92cf1f44911e |
|
||||
| suction_propagation.csv | R-raw | 9,720 | f017bcc3ca6f |
|
||||
| suction_signals.npz | R-raw | 220,058 | 66d06cf2d909 |
|
||||
| suction_summary.json | R-raw | 4,126 | d70a1d371950 |
|
||||
| validate.py | R-raw | 19,576 | 37441febd168 |
|
||||
| zero_boundary.csv | R-raw | 11,491 | 65c92e48c974 |
|
||||
| zero_collisions.csv | R-raw | 18,915 | 027cb8ff6b23 |
|
||||
| zero_ledger.csv | R-raw | 16,051 | 34a99bb89dd7 |
|
||||
| zero_signals.npz | R-raw | 127,958 | a966f093a9cd |
|
||||
| zero_summary.json | R-raw | 1,508 | 6e033f991438 |
|
||||
|
||||
## research/fixed_patch_jet_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 69,721 | 91ad9c4d1e88 |
|
||||
| before.json | R-raw | 690 | 06e1b6278a29 |
|
||||
| fixed_patch_jet.py | R-raw | 9,646 | 6fbdcd9a076b |
|
||||
| incremental.patch | D-snapshot | 9,891 | b43129d82c69 |
|
||||
| manifest.json | P-pin | 1,967 | 08daa0248f60 |
|
||||
| notes.txt | K | 2,214 | 50070fe54efb |
|
||||
| results.json | R-raw | 1,100 | d959a6a1cc2c |
|
||||
| stdout.txt | R-raw | 1,272 | eccdc0f6b16a |
|
||||
|
||||
## research/fixed_patch_jet_closure_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 72,692 | 8a157af82ecf |
|
||||
| cpu_results.json | R-raw | 628 | 8e4a1c090c93 |
|
||||
| cpu_stdout.txt | R-raw | 590 | d800b783fe47 |
|
||||
| incremental.patch | D-snapshot | 3,490 | a9a486c68afd |
|
||||
| layout_uncropped.png | R-raw | 51,785 | 0cf1cfa5e99d |
|
||||
| manifest.json | P-pin | 1,849 | 31cf0c739241 |
|
||||
| notes.txt | K | 3,023 | 0f7e7bdfd0e6 |
|
||||
|
||||
## research/fixed_patch_jet_records_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 71,228 | a8e967f76e08 |
|
||||
| before.json | R-raw | 690 | ebd6f4251fc2 |
|
||||
| fixed_patch_jet.py | R-raw | 10,650 | f6b299885da0 |
|
||||
| incremental.patch | D-snapshot | 5,539 | c7f4d0c02ccc |
|
||||
| manifest.json | P-pin | 2,057 | 1d616c2541df |
|
||||
| notes.txt | K | 2,449 | b58cec14d359 |
|
||||
| results.json | R-raw | 1,136 | 788af64a6ebd |
|
||||
| stdout.txt | R-raw | 20,750 | a765b06deefa |
|
||||
|
||||
## research/j1-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 7,667 | 8f469daef054 |
|
||||
| baseline-status.txt | P-pin | 2,601 | 64ca05ab952e |
|
||||
| baseline.json | P-pin | 23,052 | ab5852bcb0ef |
|
||||
| incremental.diff | D-snapshot | 29,912 | dd9c4e688888 |
|
||||
| results.json | R-raw | 68,294 | 56547aa8a678 |
|
||||
|
||||
## research/j2-slot-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 12,930 | 69fa4d3bccd3 |
|
||||
| baseline-status.txt | P-pin | 2,825 | e711afb0016c |
|
||||
| baseline.json | P-pin | 24,660 | 286e023da94f |
|
||||
| incremental.diff | D-snapshot | 25,231 | 649f3b5d168d |
|
||||
| results.json | R-raw | 477,249 | b4272e25179d |
|
||||
|
||||
## research/j3-coverage-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 11,409 | 5887cf73dce2 |
|
||||
| baseline-status.txt | P-pin | 3,055 | 78e88356caa3 |
|
||||
| baseline.json | P-pin | 30,407 | 095972547a7f |
|
||||
| incremental.diff | D-snapshot | 20,114 | f0d502dc5c5b |
|
||||
| negative-budget-20744.h5 | R-raw | 88,017 | 693b133be84d |
|
||||
| negative-end-20760.h5 | R-raw | 88,174 | 429270d263d4 |
|
||||
| restart-21280.h5 | R-raw | 88,062 | 74900affae38 |
|
||||
| restart-reference-21296.h5 | R-raw | 88,154 | 7510ee97885b |
|
||||
| results.json | R-raw | 296,314 | 7806b10e65cb |
|
||||
| verification.json | K | 1,468 | 78a07c47aea4 |
|
||||
|
||||
## research/j3-lag-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 11,732 | 34fe5dbb2cae |
|
||||
| baseline-status.txt | P-pin | 2,968 | 6ae6094ea9d7 |
|
||||
| baseline.json | P-pin | 26,505 | 5c289d14ff30 |
|
||||
| end-21280.h5 | R-raw | 92,366 | f30b209179ab |
|
||||
| incremental.diff | D-snapshot | 15,790 | 5c6c485e1f75 |
|
||||
| results.json | R-raw | 21,718 | 2bf58939129c |
|
||||
| start-20760.h5 | R-raw | 92,247 | ae0abd05190c |
|
||||
|
||||
## research/j3-offline-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 6,271 | a76e1b486bd2 |
|
||||
| lags.csv | R-raw | 1,104 | dd3136dd2dc2 |
|
||||
| localization.png | R-raw | 136,013 | 74b56f3e814c |
|
||||
| regions.csv | R-raw | 2,126 | 5d361e6872af |
|
||||
| results.json | R-raw | 8,852 | bf77b0f534d4 |
|
||||
|
||||
## research/j3-reservoir-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 13,563 | a74b67a64e0d |
|
||||
| baseline-status.txt | P-pin | 2,896 | a318e077dfae |
|
||||
| baseline.json | P-pin | 25,571 | 060502ea7dd8 |
|
||||
| incremental.diff | D-snapshot | 22,898 | 8e2c79a86c98 |
|
||||
| results.json | R-raw | 182,747 | 7fd5e51a1785 |
|
||||
|
||||
## research/refinement_example_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 8,286 | 591478100da8 |
|
||||
| before.json | R-raw | 818 | 7ab0962775b1 |
|
||||
| incremental.patch | D-snapshot | 8,682 | 7ad8719d0167 |
|
||||
| manifest.json | P-pin | 1,798 | bf1eee8a7cf8 |
|
||||
| notes.txt | K | 1,185 | 7ff019ad023d |
|
||||
| results.json | R-raw | 1,118 | 94697b0988b9 |
|
||||
| stdout.txt | R-raw | 325 | 501446f557f4 |
|
||||
|
||||
## research/refinement_followup_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 4,887 | af65e889f20e |
|
||||
| baseline.json | P-pin | 5,343 | 7c60ad8ad692 |
|
||||
| cleanup.json | R-raw | 1,253 | 15cf342fba2e |
|
||||
| cpu_results.json | R-raw | 1,056 | 1166dffd2a09 |
|
||||
| cpu_results_attempt1.json | R-raw | 642 | d139d5508105 |
|
||||
| cpu_stdout.txt | R-raw | 499 | d1b33ea20563 |
|
||||
| cpu_stdout_attempt1.txt | R-raw | 879 | 6a9fe7fce39a |
|
||||
| gpu_before.json | R-raw | 589 | 46762a0ab207 |
|
||||
| gpu_before_no_launch.json | R-raw | 589 | 074982d1b2d4 |
|
||||
| gpu_rectangle_before.json | R-raw | 589 | 831e3793339f |
|
||||
| gpu_rectangle_before_no_launch_1791886104981638567.json | R-raw | 589 | c33885d50bfd |
|
||||
| gpu_rectangle_results.json | R-raw | 1,003 | 9c6da219ac6c |
|
||||
| gpu_rectangle_stdout.txt | R-raw | 332 | 9185bf303954 |
|
||||
| gpu_results.json | R-raw | 1,189 | 53e4f1c44de1 |
|
||||
| gpu_seed1_before.json | R-raw | 589 | c77ea3485f84 |
|
||||
| gpu_seed1_results.json | R-raw | 996 | b718e8af84e4 |
|
||||
| gpu_seed1_stdout.txt | R-raw | 306 | 4d7de0897137 |
|
||||
| gpu_stdout.txt | R-raw | 68 | b0956fe3cdb0 |
|
||||
| incremental.patch | D-snapshot | 16,836 | 9df25d8c177a |
|
||||
| manifest.json | P-pin | 4,410 | 177704d6f1ae |
|
||||
| run_gpu.py | R-raw | 5,237 | b5af3f0d4f79 |
|
||||
|
||||
## research/refinement_prepare_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 8,939 | 8e210c28c7c5 |
|
||||
| baseline.json | P-pin | 46,348 | d5f257216b8d |
|
||||
| incremental.patch | D-snapshot | 15,751 | 0ddb32550584 |
|
||||
| manifest.json | P-pin | 2,113 | 9fd94ccf2c3e |
|
||||
| validation.json | R-raw | 3,567 | f057e7cbcd35 |
|
||||
| validation.txt | R-raw | 873 | 594daa45c58c |
|
||||
|
||||
## research/refinement_slot_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 67,717 | d498081e7c50 |
|
||||
| before.json | R-raw | 690 | 430e76b3aa8a |
|
||||
| cpu_results.json | R-raw | 1,421 | 4f6bc4d9428f |
|
||||
| incremental.patch | D-snapshot | 18,676 | 67a3e8cc9e68 |
|
||||
| manifest.json | P-pin | 2,270 | 6515458290f4 |
|
||||
| notes.txt | K | 2,612 | 08a713ba4b37 |
|
||||
| results.json | R-raw | 1,160 | 03e683027ba8 |
|
||||
| stdout.txt | R-raw | 643 | d57b91769826 |
|
||||
|
||||
## research/refinement_slot_followup_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 68,536 | 89a26ce79438 |
|
||||
| before.json | R-raw | 690 | e707df192ff2 |
|
||||
| incremental.patch | D-snapshot | 495 | c6f6fa11237d |
|
||||
| manifest.json | P-pin | 1,920 | 4840243a33aa |
|
||||
| notes.txt | K | 1,549 | 4b77583ca116 |
|
||||
| planar_velocity.cuh | R-raw | 1,911 | 87db7466c843 |
|
||||
| planar_velocity.sha256 | P-pin | 251 | 6694c4a1c1f0 |
|
||||
| results.json | R-raw | 1,050 | 6ef6eda854c6 |
|
||||
| stdout.txt | R-raw | 1,160 | 461860351c0c |
|
||||
|
||||
## research/refinement_step_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 10,671 | 88d6f24743f5 |
|
||||
| baseline.json | P-pin | 40,620 | 20f61559dd38 |
|
||||
| cpu_validation.json | R-raw | 1,231 | 7999fd30018c |
|
||||
| gpu_failure_validation.json | R-raw | 868 | 5f6db65ee4f9 |
|
||||
| gpu_failure_validation.txt | R-raw | 248 | d9a570a1e9e9 |
|
||||
| gpu_validation.json | R-raw | 672 | 2028f59e1055 |
|
||||
| gpu_validation.txt | R-raw | 632 | 9699918ee127 |
|
||||
| incremental.patch | D-snapshot | 26,516 | d95f9347633d |
|
||||
| manifest.json | P-pin | 2,075 | 32a7f1a7309e |
|
||||
|
||||
## research/repair-2026-10-10
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| TRON_HANDOFF.md | R-raw | 25,208 | a339786c7944 |
|
||||
| baseline-hashes.json | P-pin | 16,020 | 9b4e24cb1d10 |
|
||||
| baseline-status.txt | P-pin | 429 | 515be249b638 |
|
||||
| checkpoint-domain-2026-10-12.diff | D-snapshot | 14,443 | 035ed630dda1 |
|
||||
| classic-results.json | R-raw | 244,729 | 7297aa0d5bca |
|
||||
| classic-signals.npz | R-raw | 630,332 | 8cf6d1218aa0 |
|
||||
| final-gpu.log | R-raw | 191 | a0eec1a8ea31 |
|
||||
| final-unit.log | R-raw | 188 | 94efba2f1d42 |
|
||||
| provenance.json | P-pin | 1,020 | 25e22b4f0696 |
|
||||
| repair-architecture.md | K | 9,498 | 12051c3e8571 |
|
||||
| repair-numerical.md | K | 10,769 | 0c5757fe5f36 |
|
||||
| repair-state.md | K | 8,481 | 3c7f2d5b687a |
|
||||
| simplification-checks.log | R-raw | 1,045 | fa9d464be740 |
|
||||
|
||||
## research/shear_refinement
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.md | K-code | 4,142 | 67bdd8ed622b |
|
||||
| boundary_shear_tagging.py | K-code | 13,859 | b5415a1c4ea1 |
|
||||
| example.py | K-code | 6,323 | 66295d3f0273 |
|
||||
|
||||
## research/surface_actuation_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 54,139 | 0ad06888dbeb |
|
||||
| cpu_checkpoint_run.json | R-raw | 400 | 39681b60ff15 |
|
||||
| cpu_checkpoint_stdout.txt | R-raw | 114 | 929e4b8d4f46 |
|
||||
| cpu_initial.txt | R-raw | 620 | 29213c58a679 |
|
||||
| cpu_surface_command.txt | R-raw | 118 | 6d8c615a6e03 |
|
||||
| cpu_surface_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_surface_stdout.txt | R-raw | 99 | ee9237fc26e5 |
|
||||
| gpu_run.json | R-raw | 1,698 | dc8f87eafaa9 |
|
||||
| gpu_stdout.txt | R-raw | 159 | b262891e52c3 |
|
||||
| incremental.diff | D-snapshot | 50,206 | 43090a1a8b11 |
|
||||
| manifest.json | P-pin | 7,527 | 35daa01271cb |
|
||||
| report.txt | K | 5,078 | 1162a6479f9f |
|
||||
|
||||
## research/wall_seam_pulse_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| analyze.py | R-raw | 10,343 | ed7cf6999a49 |
|
||||
| interpretation.txt | K | 3,514 | 90935db2aa99 |
|
||||
| results.json | R-raw | 15,137 | 678a0759871a |
|
||||
| run.json | R-raw | 1,170 | 53e303fd0d5d |
|
||||
| sources.csv | R-raw | 2,313 | 40d3736b8ef1 |
|
||||
| stdout.txt | R-raw | 1,179 | 756d8c38e213 |
|
||||
|
||||
## tests/validation/multigrid/cpuref_0_3_evidence
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 4,133 | 62897bfd9812 |
|
||||
| README_grok.md | K | 1,751 | 2d36e05661a8 |
|
||||
| baseline.json | P-pin | 39,291 | 188491096547 |
|
||||
| manifest.json | P-pin | 3,495 | 10c19f5fe5ca |
|
||||
| results_horsea.json | R-raw | 2,281 | 3b163384e376 |
|
||||
| stdout_grok.txt | R-raw | 7,143 | 5a0c270ea4ae |
|
||||
| stdout_horsea.txt | R-raw | 9,013 | adff8bb8d295 |
|
||||
|
||||
## tests/validation/multigrid/local_stress_v1_evidence
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 8,239 | 3513fd294223 |
|
||||
| TABLE_local_stress_observable_v1.md | K | 2,967 | 6115dcde0f46 |
|
||||
| baseline.json | P-pin | 43,991 | ab631ea4f2ad |
|
||||
| comparison.json | R-raw | 13,248 | d7d1b36eb247 |
|
||||
| manifest.json | P-pin | 4,132 | c6c44d8125cd |
|
||||
| results_horsea.json | R-raw | 2,671 | 79bdf001f948 |
|
||||
| run_once.py | R-raw | 4,313 | fa9c5239e993 |
|
||||
| source_manifest.json | P-pin | 622 | 0516f1860407 |
|
||||
| stdout_horsea.txt | R-raw | 17,091 | a20c554693df |
|
||||
| stdout_local_stress_observable_v1.txt | R-raw | 12,238 | 692ffe1a05da |
|
||||
|
||||
## tests/validation/multigrid/shear_v0_2_evidence
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 7,220 | 772f74ad40ff |
|
||||
| baseline.json | P-pin | 41,751 | 70d9c9fa71c0 |
|
||||
| comparison.json | R-raw | 7,679 | 0af8736f9a01 |
|
||||
| joint_report.md | K | 6,110 | eed871221298 |
|
||||
| manifest.json | P-pin | 3,796 | b22d03a1ba9e |
|
||||
| results_horsea.json | R-raw | 1,957 | b2cd48a07a08 |
|
||||
| run_once.py | R-raw | 3,245 | 0b8d5e21f326 |
|
||||
| source_manifest.json | P-pin | 574 | 4d1c742690c7 |
|
||||
| stdout_grok.txt | R-raw | 7,950 | 0e80b1aba7f6 |
|
||||
| stdout_horsea.txt | R-raw | 11,421 | 3c867cdac197 |
|
||||
@@ -1,109 +0,0 @@
|
||||
# Fig. 2 material collection: modern moving plant
|
||||
|
||||
Research-only child of `76873d911c4e2caba1bacc25aaa1495ea768453f`.
|
||||
No production/kernel/manuscript or DynamisLab edits. These are fresh common-release
|
||||
paired experiments, separate from the continuous OFF/ON/OFF demonstration videos.
|
||||
|
||||
## Fixed plant and cases
|
||||
|
||||
All cases use D40, Uref=.005, nu=.004, MRT/FP32/unshifted double buffering,
|
||||
the existing mean-parabolic/regularized inlet, NEQ outlet and bounce-back walls.
|
||||
One transverse assembly DOF: m*=4 based on all three disks, fnD/U=.2, zeta=0.
|
||||
No force filter or retuning. Existing +/-D, speed.025, nonfinite and no-donor stops.
|
||||
Structure RHS is previous native-step sum of pinball CME_y+Wen_y. Policy uses
|
||||
SI-mean packed CME and fixed lab probe signals; these are different force paths.
|
||||
SI=3200; EMA alpha=.9**.25 resets every SI as in the acquisition contract.
|
||||
Steady video used SI4000; its descriptive numbers are not the new collection.
|
||||
|
||||
| case | prelude/release clock | controller | collection |
|
||||
|---|---|---|---|
|
||||
| steady | new static pinball, 2,048,000 steps | constant wall speeds (0,-5.1,5.1)U, no PPO | 400 SI,160 D/U |
|
||||
| Karman | verified saved static release at4,576,000 | historical `karman_re100` PPO; actual pinball Re_D50, scale8/bias(0,-4,4) | 400 SI,160 D/U |
|
||||
| Taylor y0 | clean512,000 then static pinball512,000; clock1,024,000 | `vortex_taylor` PPO, scale4/samebias | 150 SI,60 D/U |
|
||||
|
||||
Zero/control share exact current/spare populations, flags, clock and zero initial
|
||||
structural state. Target is a separate no-pinball reference at the SAME absolute
|
||||
clocks, not a copy of a solid-containing release. Its different geometry is the
|
||||
definition of the reference, not identical microscopic initial conditions.
|
||||
Karman target must run to4,576,000; old target.npy is from earlier clocks and is
|
||||
not reused for same-clock field errors. Model/norm files are verified by SHA256
|
||||
in the driver and written with all source hashes into case.json.
|
||||
|
||||
## Taylor injection
|
||||
|
||||
Exactly one event at release in every role, inside the owning synchronized CUDA
|
||||
context: centre(600,510.5), R80, strength **.000075**. Reuse the existing initializer
|
||||
unchanged. It rewrites ALL interior cells to equilibrium and uploads BOTH buffers;
|
||||
this removes existing nonequilibrium globally. The impulse and first two SI of
|
||||
native force/displacement remain in the record. This is a declared limitation,
|
||||
not an independently qualified physical vortex generator.
|
||||
|
||||
Strength has inverse-time units: u_theta=(s*pi/2)r exp(-r²/R²). D20 R40/U.01/s.0003
|
||||
maps to D40 R80/U.005/s.000075 under diffusive scaling. Using .03*U at D40 would
|
||||
double u/U and quadruple p/U²; actual-function CPU positive/negative controls lock it.
|
||||
|
||||
Current Horsea `acquisition.py` SHA
|
||||
`ea674080501407d00ded6d23467423c516cd37e04d166d5adda16465100204d3`
|
||||
defines EIGHT offsets(-20,-15,-10,-5,0,5,10,15). `legacy_test/acquire.py` SHA
|
||||
`91cd29a368dfc53a0804ec37397d2e5f25f0f8721f01b560a0f4328b8fd1dd9c`
|
||||
selects the controlled centre-uy event: median first20, peak over38..87 inclusive.
|
||||
All roles use that common event index; own target/role peak delays are separately
|
||||
reported. The earlier five-offset remote summary is superseded by actual source.
|
||||
|
||||
## Estimands and storage
|
||||
|
||||
Primary: unaligned same-clock/event velocity-vector errors, Eq1
|
||||
`sqrt(mean_ROI((dux²+duy²)/Uref²))`. No target-energy denominator. Lab ROI is
|
||||
front-equilibrium-centre-relative x/D=[4,24], y/D=[-5,5], equivalent to sensor-plane
|
||||
x/D=[-6,14]; D40 inclusive columns1360..2160 and rows311..710. All ROI cells must
|
||||
be fluid and finite in every compared field; record support coverage.
|
||||
|
||||
Motion uses native last80 D/U for steady/Karman, whole60 D/U for Taylor; mean,
|
||||
centered RMS and half-range remain distinct. Karman complete-cycle means and
|
||||
eight independently phased nearest snapshots reuse actual acquisition helpers,
|
||||
centre-uy column3 and minimum crossing gap. Missing crossings yield unavailable
|
||||
phase diagnostics, never fabricated phase slots. Steady retains its last snapshot
|
||||
and separately labeled fixed-window mean. Field collection for steady/Karman is
|
||||
the last160 SI boundaries (96.4..160 D/U); motion window remains80..160 D/U.
|
||||
Taylor retains its eight common event snapshots. Fixed-window mean error differs
|
||||
from mean instantaneous error; both are labeled. Native six-channel DTW is a
|
||||
supplementary training-space metric, never field recovery or physical delay;
|
||||
its actual legacy lag uses flat sensor column1, unlike the phase helper's column3.
|
||||
|
||||
Use one FP32 full-grid RAM cache per role, plus compact ROI RAM histories for
|
||||
same-clock comparisons. Admit only with >=16GiB MemAvailable; no3.35GB disk field
|
||||
cache. Save only selected fields, means, explicit masks, scalars and source receipts.
|
||||
Nonfluid placeholders are zero-filled for helpers but always masked in plots and
|
||||
scores; nonfinite FLUID is a hard failure. Serialize compilation/GPU work. Verify
|
||||
compressed native histories before removing only redundant task scratch copies.
|
||||
|
||||
Plot standards: vorticity curl(u)D/U, five-point fluid stencil, common RdBu_r21bins
|
||||
with fixed[-3,3] limits and nearest interpolation; lab-fixed coordinates and ROI.
|
||||
Mean plots exclude cells that were not fluid at every collected boundary. Selected
|
||||
snapshots use actual step geometry. Raw arrays and scalar errors are not clipped.
|
||||
|
||||
## V5 boundary
|
||||
|
||||
Standard V5 `kar_re100` seeds41..45 DO exist (actual pinball Re_D50), but use
|
||||
2000x600 uniform/free-slip and different absolute geometry/normalization/action
|
||||
contracts. These are not silently transferred onto the present Legacy-contract
|
||||
plant. No verified retained trained V5 steady/Taylor bundle was found; a vortex
|
||||
environment prototype is not a trained policy. Existing V5 within-case results
|
||||
may be indexed separately; cross-generation scores cannot imply an identical plant.
|
||||
|
||||
## Execution
|
||||
|
||||
First run focused CPU contracts, independent source review, then each scene with
|
||||
`--smoke` (three32-step SI per role; no physical scores). Only the reviewed exact
|
||||
SHA may run physical cases, one at a time, new outputs outside the checkout:
|
||||
|
||||
```
|
||||
CUDA_VISIBLE_DEVICES=3 PYTHONPATH=src python research/fig2_collection/run.py --scene karman --out /tmp/celeris-results-fig2-collection/karman
|
||||
python research/fig2_collection/analyze.py /tmp/celeris-results-fig2-collection/karman
|
||||
python research/fig2_collection/plot.py /tmp/celeris-results-fig2-collection/karman
|
||||
```
|
||||
|
||||
Use the same invocation for steady/taylor with new output directories. No automatic
|
||||
extension or parameter sweep. All results are developmental material, not asserted
|
||||
settled/physically qualified. Preserve partial failures and distinguish missing
|
||||
evidence from passing gates.
|
||||
@@ -1,26 +1,23 @@
|
||||
# Fixed three-level rectangle unions
|
||||
# Fixed three-level separated patches
|
||||
|
||||
This page describes three-level `FixedPatchStepper` setups built from the
|
||||
`refinements` level list, with rectangle unions on either adjacent pair. The
|
||||
same constructor supports arbitrary depth; three or more levels require
|
||||
all-blocked storage and bodies/Jets only on the finest level. This is not
|
||||
general AMR: there is no per-region timestep, moving geometry, automatic
|
||||
dynamic refinement or L1 body support in the three-level form.
|
||||
`FixedPatchStepper` supports exactly three static, blocked levels with separated
|
||||
rectangle sets on either adjacent pair. This is not general AMR: there is no
|
||||
per-region timestep, moving geometry, dynamic refinement or L1 body support.
|
||||
All regions share one current/spare buffer pair per level. The existing schedule
|
||||
advances L0 once, L1 twice and L2 four times per base step, with aggregate transfers.
|
||||
No CUDA kernel changes are involved.
|
||||
|
||||
## Explicit coordinate frames
|
||||
|
||||
For a single scalar parent patch, pass a scalar rectangle as `refinements[1]`:
|
||||
`(x0,y0,x1,y1)` in half-open integer indices of the allocated L1 window,
|
||||
including its ghost band.
|
||||
Keep the existing scalar `child=(x0,y0,x1,y1)` for a single scalar parent patch.
|
||||
Its coordinates retain their original meaning: half-open integer indices in the
|
||||
allocated L1 window, including its ghost band.
|
||||
|
||||
A rectangle list as `refinements[1]` accepts a scalar or a set parent and
|
||||
interprets every rectangle in the actual allocated L1 array, including ghost
|
||||
bands and holes. It never infers region-local coordinates. The physical root
|
||||
cell edge corresponding to an L1 index `(x,y)` is
|
||||
`parent.fine_origin + (x,y)/2`:
|
||||
The new keyword `child_rectangles_in_parent=[...]` is mutually exclusive with
|
||||
`child`. It accepts a scalar or a set parent and interprets every rectangle in
|
||||
the actual allocated L1 array, including ghost bands and holes. It never infers
|
||||
region-local coordinates. The physical root cell edge corresponding to an L1
|
||||
index `(x,y)` is `parent.fine_origin + (x,y)/2`:
|
||||
|
||||
- A scalar L1 parent has a shifted window origin.
|
||||
- A set L1 parent has origin `(0,0)` and public shape `(2*ny,2*nx)`.
|
||||
@@ -33,23 +30,20 @@ are physically equivalent to parent set `[(4,4,12,12)]` with child set
|
||||
`[(8,8,16,16)]`. Reusing the same numbers after changing the parent frame can
|
||||
move the physical region.
|
||||
|
||||
Both adjacent pairs are built from their unions, without a fixed rectangle-gap
|
||||
rule. Adjacent, overlapping and concave unions are accepted only when their
|
||||
actual transfer dependencies close. The whole child union must preserve
|
||||
inherited ACTIVE coverage, uncovered ACTIVE donors/receivers, surviving pulls, and the upstream
|
||||
Both adjacent pairs require a Chebyshev gap of at least four in their own
|
||||
parent-cell units. The whole child union must preserve inherited ACTIVE
|
||||
coverage, uncovered ACTIVE donors/receivers, surviving pulls, and the upstream
|
||||
restriction footprint. Sparse holes remain unowned; inherited INNER remains
|
||||
stream-only and OUTER remains invalid. Block sizes must divide their allocated
|
||||
level grids. Sparse storage owns every grid-aligned tile intersecting the union
|
||||
plus its four-cell ghost band; rectangle and support edges need not align with
|
||||
tile edges.
|
||||
stream-only and OUTER remains invalid. Block sizes must align with every
|
||||
support edge, including its four-cell ghost band.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
run = FixedPatchStepper(
|
||||
cfg, context=ctx,
|
||||
refinements=[[(4, 4, 20, 28), (24, 4, 44, 28)], [(16, 16, 32, 40), (56, 16, 80, 40)]],
|
||||
level_blocks=[(4, 4)] * 3,
|
||||
cfg, [(4, 4, 20, 28), (24, 4, 44, 28)], context=ctx,
|
||||
base_block=(4, 4), fine_block=(4, 4), child_block=(4, 4),
|
||||
child_rectangles_in_parent=[(16, 16, 32, 40), (56, 16, 80, 40)],
|
||||
)
|
||||
```
|
||||
|
||||
@@ -57,11 +51,11 @@ This example uses a 48x32 root. `initialize` and `download` use three Q-major
|
||||
dense arrays. Public arrays and masks still scale with the full allocated
|
||||
extent; sparse device buffers do not remove that host-memory limit.
|
||||
|
||||
Stationary whole-circle L2 bodies and Jets (`level_objects[2]`, `level_jets[2]`) use root edge
|
||||
Stationary whole-circle `child_objects` and `child_jets` use root edge
|
||||
coordinates and a single L2 boundary object. Body IDs are physical object-list
|
||||
positions, not region IDs or caller `obj_id`. Complete boundary stencils must
|
||||
avoid the transfer footprint. Change a child Jet with
|
||||
`set_jet_amplitude(index, value, level=2)` (`level` is required); L1 bodies/Jets remain unsupported.
|
||||
`set_jet_amplitude(index, value, level=2)`; L1 bodies/Jets remain unsupported.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -71,12 +65,6 @@ CPU geometry, table, pre-CUDA rejection, schedule and cleanup tests:
|
||||
PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_refinement*.py' -v
|
||||
```
|
||||
|
||||
`tests/unit/test_union_refinement.py` covers union dependencies and tile
|
||||
ownership; `tests/unit/test_hierarchy.py` covers composed geometry through four
|
||||
levels. `tests/unit/test_raw_exchange.py` checks the four-level host schedule
|
||||
with CUDA mocks. These establish topology/control-flow coverage, not numerical
|
||||
qualification at every depth or interface shape.
|
||||
|
||||
Explicit GPU gates are in
|
||||
`tests/validation/multigrid/test_refinement_three_level_multipatch_gpu.py`.
|
||||
They cover physical-frame equivalence, twins, asymmetric refinement, multiple
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
# Re_D = 50 Kármán inflow: paired pinball VIV / frozen PPO demo
|
||||
|
||||
Development experiment, frozen before results, 2026-10-31. No new production
|
||||
method, structural damping, force filter, policy training, or main merge.
|
||||
[Force-spike research index](research/moving_boundary_force_spikes.md).
|
||||
|
||||
## Plant and structure
|
||||
|
||||
Base `e9213dd2cc9cfed0e8487a3cf4ebdd94d23dc64d`; fixed-policy contract from
|
||||
`580397eaf83c85b233495791ecc51662a374a247`. Reuse `steady_pinball` config,
|
||||
coordinate map and exact held-force oscillator. D40, U=.005, nu=.004, MRT,
|
||||
2559 x 1022, FP32 unshifted double buffers. Regularized parabolic inlet,
|
||||
NEQ extrapolation outlet, bounce-back y walls (.5,1020.5); no BC changes.
|
||||
D20 coordinates map x'=2x, y'=.5+2(y-.5).
|
||||
Upstream cylinder (400,510.5), radius40, is static. Pinballs radius20 at
|
||||
(1200,510.5), (1252,540.5), (1252,480.5) share only transverse translation.
|
||||
Lab probes radius10 at (1600,590.5), (1600,510.5), (1600,430.5) stay fixed.
|
||||
|
||||
One shared `Translation`, travel ±D, max wall speed .025, existing clearance
|
||||
and per-step move guards unchanged. M2: m*=2 based on all three disks,
|
||||
M=7539.8223686, fn=.000025, K=.000186037660, C=zeta=0; release y=v=0.
|
||||
Each native step uses previous-step sum(CME_y+Wen_y), then exact oscillator
|
||||
update with held force. Wall speed is interval displacement y[n+1]-y[n].
|
||||
Cover/uncover impulses are logged, never added to the selected RHS. This
|
||||
estimator remains provisional; existing spike/convergence limitations remain.
|
||||
|
||||
## Same release state, two clocks
|
||||
|
||||
A static precursor retains the accepted Kármán sequence: upstream+probes,
|
||||
2,048,000-step warmup, 150 x 3,200-step no-pinball target samples, add static
|
||||
pinballs/sync, second 2,048,000-step warmup. Save both DDF buffers, flags,
|
||||
clock and last instantaneous force. Construct each moving arm fresh at the
|
||||
same zero-offset geometry, check flags, copy both buffers and clock, verify
|
||||
exact copy hashes. This one-time research initialization is not a moving
|
||||
checkpoint or FSI rollback. Initial policy observation and spin are zero.
|
||||
|
||||
Frozen model SHA256 `148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d`;
|
||||
normalizer `534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0`.
|
||||
Only these explicit supplied assets are accepted. Policy is CPU deterministic.
|
||||
Native policy signals: packed CME (same `read_force` path), lab area-mean
|
||||
velocity, each averaged over SI3200. Convert to original D20 training units:
|
||||
forces x2, area-mean velocity x2 x78, then frozen force-first normalization
|
||||
and clipping. Fine-grid probe pixel footprint is not exactly identical;
|
||||
using the original78 cells avoids a hidden area-count normalization change.
|
||||
The moving plant, motion-induced force, lab-frame wake displacement and D40
|
||||
resolution remain out of training distribution. No transfer guarantee.
|
||||
|
||||
A=(8a+(0,-4,4))*.005; EMA alpha=.9^(1/4), reset0 each SI; omega=+A_eff/20.
|
||||
This preserves historical physical actuator timescale under dt/4.
|
||||
The zero arm substitutes a=(0,.5,-.5), so all spins are identically zero.
|
||||
Policy interval averaging does **not** average the native structural RHS.
|
||||
|
||||
## Bound, output, and interpretation
|
||||
|
||||
One serial pair, zero then controlled: 400SI=1,280,000 native steps=160D/U
|
||||
=32Tn per arm. No retuning/automatic extension. Stop on nonfinite state,
|
||||
no_donor, travel/speed/move rejection, or four-hour whole-pair wall cap.
|
||||
A failure stops the remaining run; retain valid rows and completed intervals.
|
||||
NaN trailing rows are unexecuted, not zero-valued observations.
|
||||
|
||||
Report first/last80D/U windows, mean offset separately from centered RMS,
|
||||
half-range, peak, frequency and drift; native versus SI-averaged force,
|
||||
CME/Wen/node/crossing terms and action/observation clipping. Last80D/U is
|
||||
the principal descriptive window, not an asserted settled window. If zero
|
||||
arm half-range <.1D there, classify weak response and do not claim vibration
|
||||
suppression. Report continuous ratios regardless. Cloaking uses retained
|
||||
canonical last30 lagged sensor DTW against this precursor's no-pinball target,
|
||||
with fixed-body D20 results contextual only. These are development criteria,
|
||||
not paper uncertainty, grid convergence, or proof of validated FSI.
|
||||
|
||||
Save native18-column rows (~184MB/arm), small SI signals, target, config,
|
||||
release two buffers (~188MiB), hashes and terminal reports outside checkout.
|
||||
No per-step field output. Expected new data <650MB, check actual resources.
|
||||
|
||||
```
|
||||
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src python research/karman_viv/run.py \
|
||||
--model /tmp/celeris-karman-viv-assets/model.zip \
|
||||
--norm /tmp/celeris-karman-viv-assets/norm.json --out /tmp/NEW_OUTPUT
|
||||
```
|
||||
|
||||
First `--smoke`: same D40 geometry, warmup32/target2x8/warmup32,
|
||||
4x8 response steps each arm; strictly plumbing only. CPU source checks and
|
||||
independent exact-tip review precede GPU. Preserve original DynamisLab files.
|
||||
|
||||
## One predeclared m*=4 exploration (2026-10-31)
|
||||
|
||||
The m*=2 attempt at `516975ef6482f983ace347cb10de7450d2fbd680` stopped
|
||||
at the declared travel guard in both arms: zero 10.469375 D/U, PPO 27.5525 D/U.
|
||||
Its principal 80–160 D/U paired window is unavailable. Early attenuation is
|
||||
not sustained suppression; control-only DTW is not a paired cloaking result.
|
||||
Those results remain unchanged and retained.
|
||||
|
||||
The sole next candidate doubles M and K together: m*=4, M=15079.644737231,
|
||||
K=0.000372075320164, fn=0.000025, C=zeta=0. This increases inertia and stiffness,
|
||||
not damping. Half response under an identical prescribed external force is
|
||||
the selection rationale only; coupled flow feedback can invalidate it. This
|
||||
is an exploratory development case, not independent validation or a sweep.
|
||||
|
||||
Reuse the exact saved static release, config, target, model and normalizer.
|
||||
`--release-dir` checks precursor metadata, config equality, both DDF/flags
|
||||
hashes and clock against the original transfer receipt. Each fresh arm still
|
||||
checks its upload against that seed. No warmup, moving restart or new release
|
||||
file is generated. All fluid/geometry/EMA/SI/RHS settings, 400SI per arm,
|
||||
zero damping, ±D guard and other stop conditions above remain unchanged.
|
||||
The original failure stops the remaining run rule also remains unchanged.
|
||||
|
||||
```
|
||||
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src python research/karman_viv/run.py \
|
||||
--model /tmp/celeris-karman-viv-assets/model.zip \
|
||||
--norm /tmp/celeris-karman-viv-assets/norm.json --out /tmp/NEW_M4_OUTPUT \
|
||||
--mass-ratio 4 --release-dir /tmp/celeris-results-karman-viv-516975e/pair
|
||||
```
|
||||
|
||||
Report matched valid windows only, separately retaining mean offset,
|
||||
centered RMS, half-range, interval/native forces, crossing/diagnostic counts
|
||||
and paired DTW only where both arms have sufficient data. The larger
|
||||
controlled force excursions remain unresolved: retained m*=2 data strongly
|
||||
associate them with the post-action-reset ramp, not exclusively current-step
|
||||
node switching. No force smoothing, action-contract change, enlarged travel
|
||||
band or further parameter adjustment follows an unsuccessful outcome.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Device state lifecycle (uniform Simulation, FixedPatchStepper)
|
||||
|
||||
State is grouped by what a change invalidates. This file describes the code as
|
||||
it is; it adds no framework. Line references are to candidate 0f8fdbc3.
|
||||
|
||||
| Class | Contents | How it changes | Owner rule |
|
||||
|---|---|---|---|
|
||||
| 1. Module and its globals | Compile-time (visible to the compiler, fixed per module): `config.h` macros incl. `NX`/`NY`, `COLLISION_MODEL`, `BLOCK_ROOT_BC`, `WALL_*`. Runtime per-module globals (not visible to the compiler; written by host code): `__constant__` `d_params`, `d_w`, `d_cx`, `d_cy`, `d_block_channel`, `d_block_wall`; plus bound kernel handles | Globals are written once by the owner right after `load_module`. Uniform grid: `Simulation._recompile` (construction, body-topology sync) loads a new module and re-uploads; `update_runtime_params` rewrites `d_params` (omega, force, n_objects) between runs. Steppers: never rewritten after construction, so they are immutable for the instance's lifetime although they are runtime values. The blocked slot/reservoir root reads its slot from the `d_block_wall` constant (the former dense route used `WALL_*` macros; both were bit-identical in the parity gates). | Every owner loads its own module (`compiler.load_module` = `module_from_file`, no cache). A loaded module is never shared between live owners: globals are per module, so sharing would share mutable `d_params`. |
|
||||
| 2. Geometry, layout, tables | flags, curved-link tables, block layouts and halo pairs, transfer (explode/coalesce) tables, block kinds | Immutable for the life of an instance, except translating circles on the uniform grid: their swept-band flags and cut links are rewritten on the device every advancing step (`MovingCircleCells`/`MovingCircleLinks`, docs/moving_bodies.md); the host flag mirror is then stale for those cells. Uniform grid otherwise: rebuilt by body sync. `FixedPatchStepper`: a geometry change is a new instance (`remap_finest`), committed only after a successful transfer. | No in-place table edits. |
|
||||
| 3. Controls and dependent boundary columns | Jet amplitude -> per-link `surface_velocity`; body actions (uniform grid) | Between steps, without a table or module rebuild (`set_jet_amplitude`). | Input (index, level, amplitude) is validated before any device write; invalid input changes nothing and the instance stays usable. The host copy commits only after the upload returned. Any error after validation leaves the device column undefined and fails the instance: `FixedPatchStepper` status `failed`, `Simulation._failed`; every later device-touching public method (`Simulation`: `stream`, `set_jet_amplitude`, `initialize`, `sync_bodies`, `run`/`step`, `read_surface_exchange`, `read_body(s)`, `get_macroscopic`, `get_ddf`, `update_runtime_params`, `snapshot`, `restore`, save/load checkpoint) raises; cached host readers and host-side staging (e.g. `get_flags` without translating circles, `add_body`/`remove_body` before a sync) do not touch the device and stay callable; with translating circles `get_flags` reads the device flags back and refuses after a failure. Translating-circle targets follow the same rule: `set_body` validates before mutation, the applied offset commits only after a successful synchronized step, and a failed moving step fails the instance (docs/moving_bodies.md). `close()` still releases resources and stays retryable. |
|
||||
| 4. Per-step state | population double buffers and their parity, raw-exchange ledgers, observations | Device-only during `step(n)` / `run(n)`; buffer swaps are host-handle swaps. | Valid on the host only after the single wait at the end of the call. |
|
||||
|
||||
Storage routes are separate concerns:
|
||||
|
||||
- Original uniform grid (`uniform.cu`, `Simulation`): whole-domain `NX`/`NY`.
|
||||
- Blocked levels (`blocked.cu`; every `FixedPatchStepper` level, including the
|
||||
single level of `refinements=[]`): `NX`/`NY` are one block window including halo.
|
||||
- Dense refinement levels (`refinement.cu`) were removed: `level_blocks` is
|
||||
required and every refinement level is blocked.
|
||||
|
||||
A single-rectangle refinement (`FixedPatch2D`) is stored in its own fine
|
||||
window frame (`FixedPatchStepper.__init__`,
|
||||
`BlockLayout2D.build(*pair.fine_shape, ...)`). Only a rectangle set
|
||||
(`FixedPatchSet2D`) uses the level-wide frame `(2*ny, 2*nx)`, whose
|
||||
`fine_layout` requires that full grid to be divisible by the tile
|
||||
(`refinement.py:240-252`).
|
||||
@@ -1,451 +0,0 @@
|
||||
# Moving circles on the uniform dense grid
|
||||
|
||||
## Status and user guide (current)
|
||||
|
||||
Development-stage feature, integrated from the reviewed slices S1–S3a
|
||||
(production code identical to `653fd997`). Physical evidence and its limits:
|
||||
`docs/validation/moving_bodies_stage1.md`. The force used as a structural RHS
|
||||
is a declared stage choice, not an established result.
|
||||
|
||||
*What it does.* Translating circles (with optional spin) on the dense
|
||||
double-buffer D2Q9 path: per-step cut links from an FP64 geometry record
|
||||
(M2 `MovingCircleLinks`), reclassification and refill of covered/uncovered
|
||||
cells (M1 `MovingCircleCells`), moving-wall Bouzidi with wall velocity
|
||||
`u + omega x r`, and separate per-body diagnostics.
|
||||
|
||||
*API.*
|
||||
- `Simulation.add_body("circle", center=(x, y), radius=r,
|
||||
translation=Translation(offset_box, max_speed))` before `initialize()`;
|
||||
`offset_box = (x_min, x_max, y_min, y_max)` in lattice cells around the
|
||||
base centre (must contain 0), `0 < max_speed ≤ .5`.
|
||||
- `set_body(id, offset=(dx, dy), vx=..., vy=..., omega=...)` sets a pending
|
||||
target (validated against the applied offset: box, Euclidean step ≤ .5
|
||||
cell, speed, no sliding hold); `vx`/`vy` are the interval wall velocity.
|
||||
- `run(1)` advances a pending target and commits it only after the step
|
||||
synchronizes with `no_donor == 0`; `run(n > 1)` with a pending target
|
||||
raises; without a target `run(n)` holds the geometry (spin allowed).
|
||||
Direct `LBMStepper.step` refuses while a target is pending.
|
||||
- `moving_diagnostics()` returns per body the columns of
|
||||
`MovingTables.DIAG` (CME, Wen term, link mass, cover/uncover momentum and
|
||||
mass, eq_only, no_donor, crossings) accumulated since obs were zeroed;
|
||||
`obs`/`read_force` keep their contract (CME on the body).
|
||||
- `get_flags()` reads device flags back when moving circles exist.
|
||||
- `snapshot()` / `restore()` save and restore populations only, keeping the
|
||||
current clock, controls, observations, geometry and flags. Use them only
|
||||
with unchanged geometry; restoring across a translation is unsupported
|
||||
and is not rejected at runtime. They do not provide FSI rollback.
|
||||
|
||||
*Supported:* dense double buffer, D2Q9, FP32 unshifted, SRT/MRT, circles,
|
||||
spin. *Rejected before mutation:* EsoPull, ddf shifting, D3Q19, refinement /
|
||||
blocked steppers, Jets or surface actuation on a moving circle,
|
||||
`sync_bodies` and checkpoints while a moving circle exists, swept bands that
|
||||
touch the domain edge, other bodies, sensors or force regions.
|
||||
|
||||
*Failure lifecycle.* Device-side failures (upload, step, sync, no_donor)
|
||||
mark the Simulation failed; later run/step/sync/checkpoint calls and direct
|
||||
stepper calls refuse until `close()`.
|
||||
|
||||
*Known limitations.* No mass correction at curved walls (static or moving);
|
||||
cover/uncover and Wen terms are diagnostics, not a selected force law; the
|
||||
host loop costs a fixed per-step synchronization (≈ 1 ms/step measured on a
|
||||
7.2 M-cell grid, run(1) class); outer boundaries are the single dense
|
||||
channel package (see `configs/CONFIG.md`).
|
||||
|
||||
## Design record (staged contract, kept as written)
|
||||
|
||||
The sections below are the contract the slices were reviewed against
|
||||
(S0 → S3a). They are kept unchanged for provenance; where they say "draft",
|
||||
"proposed" or "until S2/S3", the status above supersedes them.
|
||||
|
||||
Original scope (Frank via TRON, thread C0C78UFKGRM/1793335557.460509):
|
||||
uniform-grid prescribed translation and single-cylinder free transverse VIV;
|
||||
base main `b2034f87e03656c5f67ab88562a70d6c338b9918`.
|
||||
|
||||
## 1. Working standards
|
||||
|
||||
- Lean, readable kernels in the existing idiom (`kernels/step/*.cu`, shared
|
||||
`apply_bouzidi_link`); Python manages state and validation. No framework,
|
||||
no compatibility layer, no new dependency.
|
||||
- The existing stationary/rotating path (`CurvedBoundaryKernel`, host link
|
||||
packing, `sync_bodies`) stays byte-identical. Moving circles are a separate,
|
||||
explicitly enabled path with its own numerical contract (this file).
|
||||
- One authority per state (section 4). No per-step full-domain download,
|
||||
rebuild or recompile; local swept-band device updates are in scope.
|
||||
- Small slices (S1 host spec, S2 kernels without motion, S3 motion + refill,
|
||||
S4 oscillator); each reviewed SHA is frozen before targeted runs.
|
||||
- Tolerances, benchmark parameters and the force composition used in the
|
||||
structural RHS are declared before the runs that judge them. Fresh vs carried
|
||||
evidence and existing baseline failures are recorded; smoke, convergence and
|
||||
physical validation are reported separately. No undeclared force filtering.
|
||||
- External code (waLBerla GPLv3, FluidX3D source-available, VIVSIM) is a
|
||||
concept reference only; nothing is copied.
|
||||
|
||||
## 2. Current update order (main b2034f87, dense double buffer)
|
||||
|
||||
`Simulation.run(n)`: upload host action (if `upload_act`) → zero obs (if
|
||||
`zero_obs`) → `DoubleBufferStepper.step(n)` → download obs → synchronize.
|
||||
|
||||
Per step (`lbm/stepper.py` `step`):
|
||||
|
||||
1. `CurvedBoundaryKernel` over the packed cut links: reads post-collision
|
||||
`G^{n-1}` at `k_f` (and `k_f - c` for q < 1/2), adds the moving-wall term
|
||||
(`Uw = -omega*ry, Vw = omega*rx` + optional per-link `surface_velocity`),
|
||||
writes the reflected value into slot `opp` of the solid node `k_s`, and
|
||||
accumulates `c(f_toward + f_reflected)` and torque into `obs`.
|
||||
2. `OneStep` pull/collide `ddf → temp` (collides FLUID cells; obstacle
|
||||
interiors flagged `SOLID|OBSTACLE|BC_CURVED`), then buffer swap.
|
||||
3. `ForceRegionKernel`, `SensorKernel`.
|
||||
|
||||
Raw `obs` force is the force **on the body** (link derivation in section 6;
|
||||
DFG docs report `Cd > 0` from it). Translation `vx, vy` in the action slots is
|
||||
currently stored but not read by any curved kernel.
|
||||
|
||||
## 3. Proposed update order (moving circles)
|
||||
|
||||
Per step n, before the existing step 1:
|
||||
|
||||
- **M1 `MovingCircleCells`** (one thread per swept-band cell): classify with the
|
||||
analytic predicates `in_old = inside(x, c^{n-1})`, `in_new = inside(x, c^n)`;
|
||||
write `flag = in_new ? SOLID|OBSTACLE|BC_CURVED : FLUID`; covered cells
|
||||
(`!in_old && in_new`) accumulate `node_cover`; fresh cells
|
||||
(`in_old && !in_new`) are refilled (section 5) and accumulate `node_uncover`.
|
||||
- **M2 `MovingCircleLinks`** (one thread per band cell × direction): compute
|
||||
q and hit point at `c^n` (FP64, same formula as `find_circle_intersection`,
|
||||
same clamp [0.01, 1] and fallback classes as `CircleGeometry.build_cut_links`),
|
||||
skip non-cut slots, call `apply_bouzidi_link` with
|
||||
`u_w = (vx^n, vy^n) + omega × (hit - c^n)`, accumulate the link terms
|
||||
(section 6).
|
||||
|
||||
Then the unchanged steps 1–3. M2 must follow M1 because a q < 1/2 link may
|
||||
read `k_f - c` at a fresh cell.
|
||||
|
||||
Exclusivity: moving bodies are never packed into the `CurvedBoundaryKernel`
|
||||
link arrays (`n_curved` covers stationary/rotating bodies only), so M2 is the
|
||||
sole writer of their solid slots and the sole source of their link force; no
|
||||
double write and no double `obs`. The stationary path's packing, launch and
|
||||
arithmetic are unchanged.
|
||||
|
||||
Completeness: after M1, every (fluid cell, direction) pair in the swept band
|
||||
whose neighbour is `in_new` is visited by M2. This holds because the band is
|
||||
the circle AABB at the declared extreme offsets plus 2 cells, and the host
|
||||
rejects per-step moves with `|c_new - c_old|_2 > 0.5` and bands touching the domain edge,
|
||||
other bodies, sensors or force regions. Each M2 write lands at
|
||||
`index_f(k_s, opp)`, the exact slot the adjacent fluid cell pulls, so every
|
||||
fluid pull from a solid slot of a moving body is BFL-written.
|
||||
|
||||
## 4. State, masks and authority
|
||||
|
||||
- Host `ObjectState` is the single authority for one displacement
|
||||
`offset = (dx, dy)` relative to the immutable base centre, plus
|
||||
`velocity = (vx, vy)` and `omega`. Centres are always derived
|
||||
(`c = base + offset`), never stored as independent state. Velocity is
|
||||
supplied, never differenced from offsets.
|
||||
- `c^{n-1}` is derived from the last *committed* offset, `c^n` from the
|
||||
offset being applied. Per body a small device **motion record**
|
||||
`{c^{n-1}, c^n, u^n}` is uploaded with the action buffer; action-slot meaning
|
||||
is not overloaded. (CANDIDATE layout; OPEN.)
|
||||
- Fail-closed commit: an enqueued upload is not a completed one. The new
|
||||
offset becomes the committed offset only after the stream synchronization
|
||||
that follows the upload and the step group succeeds. Any upload or later
|
||||
synchronization failure leaves host and device motion state unknown: the
|
||||
simulation is marked unusable (existing `_require_usable` lifecycle), and
|
||||
no API may report a host/device match afterwards.
|
||||
|
||||
### 4a. Applied geometry vs pending target (CANDIDATE, narrow API)
|
||||
|
||||
Two distinct facts per moving body, one authority each:
|
||||
`applied` = offset of the geometry last applied on the device, changed only
|
||||
by a successful commit; `pending` = an optional requested target
|
||||
`(offset, velocity)`, changed only by `set_body`. A setter returning proves
|
||||
nothing about the device. The uploaded motion record is derived each run:
|
||||
`{c_old = base + applied, c_new = base + (pending.offset if pending is not
|
||||
None else applied), u = pending.velocity if pending is not None else 0}`
|
||||
(explicit `is not None`, so a zero offset or zero velocity is a valid target). Translational wall velocity exists only on a step
|
||||
that advances to a pending target; a hold step (`c_old == c_new`) has
|
||||
translational `u_w = 0` (spin unchanged), so a non-advancing circle never
|
||||
acts as a sliding wall. One moving step consumes one target.
|
||||
|
||||
| situation | contract |
|
||||
|---|---|
|
||||
| `set_body` target | validated against `applied` (box, `|Δc|_2 ≤ 0.5` cell, speed bound) before mutation; on success replaces `pending` |
|
||||
| target with `offset == applied` and nonzero translational velocity | `ValueError` before mutation (would be a hold step with sliding wall velocity) |
|
||||
| two setters before a run | last wins; each validated against `applied`, not against the earlier pending target |
|
||||
| `run(1)` with pending | upload record, step, sync; on success `applied := pending.offset` (applied stores only the offset), `pending := None` |
|
||||
| `run(1)` without pending | hold: `c_old == c_new`, no cover/uncover, links at `applied`, translational `u_w = 0` |
|
||||
| `run(n > 1)` with pending | `ValueError` before mutation (single-step-only moving API) |
|
||||
| `run(n > 1)`, no pending | allowed: hold for n steps, translational `u_w = 0`, spin only |
|
||||
| `run(0)` | no motion upload, no commit; `pending` kept |
|
||||
| validation failure | `ValueError`, nothing mutated, retry allowed |
|
||||
| upload/launch/sync failure after enqueue | simulation unusable; `applied` and `pending` are not reported as device truth; no re-entry |
|
||||
|
||||
Displacement norm: Euclidean per-step `|c_new - c_old|_2 ≤ 0.5` lattice
|
||||
cells (declared bound; CANDIDATE value).
|
||||
- Membership is never read from a flag that M1 is overwriting. The host
|
||||
verifies at `add_body` that every swept-band cell has base flag FLUID, so the
|
||||
band's base state is known without reading `flag_gpu`.
|
||||
- `flag_gpu` is the device authority for band cells while a moving body
|
||||
exists; the host flag mirror is stale for those cells and is not used.
|
||||
|
||||
## 5. Refill of fresh cells (CANDIDATE)
|
||||
|
||||
- Stored time level: everything in `ddf` at M1 is post-collision `G^{n-1}`; the
|
||||
refill writes all 9 slots at that same level, so every slot of a fresh cell
|
||||
that the following pull or a q < 1/2 link may read is defined. Donor values
|
||||
are read at the same level `G^{n-1}` (old-fluid cells, untouched by M1).
|
||||
- `G = f_eq(rho_bar, u_w(x)) + G_neq(x_d)` where `rho_bar` = mean density of
|
||||
valid neighbours and `x_d` = the valid neighbour best aligned with the outward
|
||||
normal `(x - c^n)/|x - c^n|`; `G_neq(x_d) = G(x_d) - f_eq(rho(x_d), u(x_d))`.
|
||||
- Valid donor: inside the domain, base FLUID, `!in_old && !in_new`.
|
||||
- Sets: `V(x)` = the 8 lattice neighbours that are in the domain, base FLUID
|
||||
and `!in_old && !in_new`. `rho_bar` = mean density over `V(x)`. NEQ donor =
|
||||
the member of `V(x)` with the largest `c·n` among those with `c·n > 0`
|
||||
(`n` = outward normal at `c^n`); none → NEQ part 0 (counted as `eq_only`).
|
||||
- `V(x)` empty → no defined density: the cell is refilled with
|
||||
`f_eq(RHO, u_w(x))` so all 9 slots stay defined, the `no_donor` counter is
|
||||
incremented, and the host treats `no_donor > 0` after the run as a hard
|
||||
failure (simulation unusable). It is not a silent fallback.
|
||||
- Non-emptiness is NOT assumed from the displacement bound. Geometry
|
||||
assumptions: circle radius `R ≥ R_min` (CANDIDATE 4 cells), `|Δc|_2 ≤ 0.5`.
|
||||
S1 includes a host enumeration over sub-cell centre positions and
|
||||
displacement directions on a fine grid within these bounds that checks every
|
||||
fresh cell has nonempty `V(x)`; it is evidence for those bounds, not a
|
||||
proof, and runtime keeps the counter.
|
||||
- Fresh cells only write, old-fluid cells only read: no race.
|
||||
|
||||
## 6. Discrete momentum ledger (to review; not a selection)
|
||||
|
||||
Notation per step n: `F^n` = fluid set after M1; links L = (k_f, dir) at
|
||||
`c^n`; `f_t = G_dir(k_f)`; `f_r` = value M2 writes into `(k_s, opp)`.
|
||||
Fluid momentum before streaming:
|
||||
|
||||
P_pre = P(G^{n-1} on F^{n-1}) - sum_cover sum_q c_q G_q + sum_fresh sum_q c_q G_q^refill
|
||||
|
||||
During the pull, every population entering a fluid cell comes from a fluid
|
||||
cell except the BFL-written slots; `f_t` leaves into solid and is never pulled
|
||||
by a fluid cell. Collision conserves momentum for SRT/MRT without forcing.
|
||||
Hence, in exact arithmetic,
|
||||
|
||||
P_fluid^n - P_fluid^{n-1} = - sum_L c (f_t + f_r) - I_cover + I_uncover + B^n + S^n
|
||||
|
||||
with `B^n` external-boundary momentum (inlet/outlet/walls) and `S^n` forcing.
|
||||
So **`F_CME + node_cover - node_uncover`** is, by construction, the body
|
||||
momentum that balances the fluid ledger of *this* storage/update order.
|
||||
Branch details: halfway fallback links (`f_r = f_t` plus the moving-wall
|
||||
addend) and q < 1/2 links enter the link accumulators exactly like Bouzidi
|
||||
links, with `c(f_t + f_r)`. In the q < 1/2 branch `G_dir(k_f - c)` is an
|
||||
interpolation input only, not a fluid-momentum sink. Pull-slot completeness
|
||||
is the section 3 contract. The scope excludes `ddf_shifting` (rejected), so
|
||||
the unshifted force form applies. The balance is a fluid-side identity; it
|
||||
does not close body+fluid globally in a domain with inlet/outlet/forcing
|
||||
(`B^n`, `S^n`), and the solid-slot writes are boundary plumbing, not physical
|
||||
streaming packets.
|
||||
|
||||
The Wen 2014 relative-velocity link sum adds `-u_w (f_t - f_r) = u_w dm` per
|
||||
link (`dm = f_r - f_t`, the BFL mass defect). That term has no counterpart in
|
||||
the fluid ledger above. Peng et al. (PRE 2016 submitted, §II.C, Eqs. 20–21)
|
||||
show for a straight wall that the bounce-back error accumulated before a node
|
||||
switch cancels with the switching momentum. Whether `sum u_w dm` approximates
|
||||
`I_cover - I_uncover` here (implying double counting if both are used) is
|
||||
**NOT demonstrated** for this implementation: it needs a bounded experiment
|
||||
(prescribed slow translation; cumulative `sum u_w dm` vs cumulative node
|
||||
impulse, per crossing cycle). Momentum conservation is also distinct from
|
||||
Galilean invariance, which G4 tests.
|
||||
|
||||
External reference note: waLBerla's covered/refill momentum switches
|
||||
(`conserveMomentumWhenMapping`, `conserveMomentumWhenRestoring`) default to
|
||||
false (Astra), so their existence does not establish which combination its
|
||||
validation cases use. A bookkeeping identity alone does not select the
|
||||
physical force either.
|
||||
|
||||
Mass ledger (same proposition status). With `M^n` the active-fluid mass
|
||||
summed over `F^n` at the sample point,
|
||||
|
||||
M^n - M^{n-1} = sum_L dm - m_cover + m_uncover + B_m^n
|
||||
|
||||
(`dm = f_r - f_t`, `m_cover`/`m_uncover` = summed `sum_q G` of covered and
|
||||
refilled cells, `B_m` external-boundary mass flux; collision conserves mass).
|
||||
|
||||
Sample point (fixed): post-collision storage after the swap of step n, i.e.
|
||||
the same level `G` that M1 reads at step n+1; both ledgers are evaluated
|
||||
there. Ledger residual = left side minus every listed term. Zero mass flux
|
||||
does not imply zero momentum exchange: closed walls (bounce-back, free-slip)
|
||||
still exchange momentum, so "no inlet/outlet, no forcing" does NOT give
|
||||
`B = S = 0`. The rounding-level momentum check therefore uses either a
|
||||
momentum-isolated case (periodic in both directions, no forcing) or counts
|
||||
the wall impulse explicitly as part of `B`; the mass check may use a closed
|
||||
domain. Otherwise `B`, `S` are not separately measured, the residual is
|
||||
reported as their estimate and is not a pass criterion.
|
||||
Both ledger identities are **proposed, unreviewed**: they are not called
|
||||
proven until review and a host replay agree.
|
||||
|
||||
Accumulators (separate, per body, per step window; all reported raw):
|
||||
`link_cme`, `link_wen_correction` (= `sum u_w dm`), `node_cover`,
|
||||
`node_uncover` (momentum), `link_dm`, `mass_cover`, `mass_uncover` (mass),
|
||||
plus counters of crossings, `eq_only` and `no_donor`, so force spikes can be
|
||||
correlated with crossing events.
|
||||
|
||||
**OPEN — structural RHS candidates (not selected):**
|
||||
(a) `link_cme + node_cover - node_uncover`; (b) `link_cme + link_wen_correction`
|
||||
(no node impulse). Selection follows the ledger review and the bounded
|
||||
experiment above.
|
||||
|
||||
## 7. Rejection semantics
|
||||
|
||||
- Before any mutation (`ValueError`, no device write, host state unchanged):
|
||||
moving body with EsoPull, `ddf_shifting`, refinement/blocked stepper, D3Q19,
|
||||
Jets or surface actuation on that body, `sync_bodies`/checkpoint while a
|
||||
moving body exists; swept band touching domain boundaries, other bodies,
|
||||
sensors or force regions; `set_body` offset outside the declared box,
|
||||
per-step move `|c_new - c_old|_2 > 0.5` cell (the single bound of 4a), speed
|
||||
above the declared bound.
|
||||
- Host motion state is committed only after the upload *and* the following
|
||||
synchronization of the step group succeed (section 4); any failure in
|
||||
between makes the simulation unusable and no host/device match is reported.
|
||||
- The only soft device diagnostic is `eq_only` (NEQ part omitted, counted).
|
||||
`no_donor > 0` is a hard failure: it is checked after the step group and
|
||||
before the step is committed or any results are returned; on failure the
|
||||
simulation is unusable (section 5).
|
||||
|
||||
## 8. Coupling and units (CANDIDATE)
|
||||
|
||||
Lattice units, rho0 = 1, per unit span. Step n uses `c^n`, `u^n` and yields
|
||||
`F^n`. Weak staggered update
|
||||
`v^{n+1} = v^n + (F^n - c v^n - k y^n)/m`, `y^{n+1} = y^n + v^{n+1}`
|
||||
(Newmark is an alternative; OPEN). Mass, natural frequency, damping and `U_r`
|
||||
follow the chosen benchmark's own definitions. Leontini, Thompson & Hourigan,
|
||||
JFS 22:857–864 (2006), DOI 10.1016/j.jfluidstructs.2006.04.003, as read by
|
||||
Grok from the full text: Re = 200, `m* = m/m_f = 10` (m_f displaced fluid
|
||||
mass), `zeta = c/c_crit = 0.01`, `U* = U/(f_N D)` with `f_N` the natural
|
||||
frequency in fluid assuming an inviscid added-mass term; the paper also
|
||||
reports frequencies normalised by the in-vacuo value. Our runner derives `k`
|
||||
from that in-fluid `f_N` (added-mass coefficient as stated in the paper) and
|
||||
never from the vacuum formula; `c_crit` convention and the original
|
||||
comparison data are still OPEN. Stability/convergence is gated by fluid
|
||||
time/space refinement of the coupled run, not assumed from the mass ratio;
|
||||
structural sub-stepping does not substitute for fluid-coupling time
|
||||
refinement.
|
||||
|
||||
## 9. Gates (map; tolerances OPEN, declared before runs)
|
||||
|
||||
G0 zero-motion regression, split: G0a populations of the existing stationary
|
||||
and spin-only path bitwise unchanged vs main; G0b moving path at rest (with
|
||||
and without spin) reproduces the stationary link set and populations; force
|
||||
estimator outputs are compared separately from populations, since the moving
|
||||
path adds estimator terms · G1 raw force sign (`Cd > 0`) ·
|
||||
G2/G3 prescribed translation incl. ledger accumulators · G4 matched
|
||||
relative-flow (moving body in quiescent fluid vs fixed body in uniform
|
||||
stream), optional boost, co-moving sanity · G5 Dütsch et al. 1998 Re 100,
|
||||
KC 5 in-line (needs 2-component offset) · G6 Leontini free transverse VIV
|
||||
(m* = 10, ζ = 0.01, Re = 200, definitions pending) · G7 time-level contract
|
||||
frozen before the oscillator.
|
||||
|
||||
Candidate pre-run engineering tolerances (Astra; for discussion, not
|
||||
literature limits or measured outcomes; norms, averaging window, convergence
|
||||
protocol and physical reference uncertainty must be fixed before adoption, and
|
||||
none may be weakened after seeing runs):
|
||||
normalised momentum-ledger residual FP32 ≤ 5e-6 (FP64 oracle ≤ 2e-13);
|
||||
10-cycle relative mass drift ≤ 1e-4; prescribed-motion grid study D = 40/80/160
|
||||
with change in the force-response amplitude (observed quantity and norm to be
|
||||
named before adoption) ≤ 2 % and phase ≤ 2°; enlarged domain
|
||||
amplitude change ≤ 1 %; matched-frame force RMS ≤ 2 % and improving with
|
||||
refinement; VIV refinement amplitude ≤ 2 %, frequency ≤ 1 %, five consecutive
|
||||
cycle amplitudes within 1 %.
|
||||
|
||||
Conditions stated before any moving kernel (S2/S3), not only at the
|
||||
oscillator stage: (i) fluid refinement protocol for the prescribed-motion
|
||||
gates — diffusive scaling (dx halved, dt quartered, physical Re, KC and
|
||||
amplitude fixed), the observed quantities and norms named in advance;
|
||||
(ii) Galilean constant-boost G4-boost requires boundaries compatible with a
|
||||
uniform boost (periodic in the boost direction or boundaries that move with
|
||||
it); if the current dense configuration cannot represent that, G4-boost is
|
||||
recorded as not runnable rather than replaced by another test.
|
||||
|
||||
## 10. Proposed minimal S1 scope (host only; held until cross-review)
|
||||
|
||||
- `add_body` moving-circle declaration: base centre, declared offset box
|
||||
(x/y), speed bound; swept-band computation and band-placement validation.
|
||||
- `set_body(offset=..., velocity=..., omega=...)` validation before mutation
|
||||
(section 7), applied/pending state and the run-transition table of 4a,
|
||||
and the fail-closed commit of section 4.
|
||||
- Host enumeration check of refill-donor non-emptiness (section 5) within
|
||||
the declared `R_min` and displacement bound.
|
||||
- Motion-record host layout and upload path; exclusion of moving bodies from
|
||||
the packed `CurvedBoundaryKernel` links (stationary packing unchanged).
|
||||
- Explicit rejections for unsupported modes.
|
||||
- S1 execution guard: until the S2 moving kernels exist, any `run(n)` with
|
||||
`n > 0` (and `step(n)`) while a moving body is declared raises before any
|
||||
device mutation (no action/motion upload, no launch), including the
|
||||
nominal at-rest case, because the moving body is excluded from the static
|
||||
packed links and nothing else would apply its boundary. `run(0)` keeps its
|
||||
existing behaviour. Simulations without moving bodies are unaffected.
|
||||
Preparation/commit helpers are exercised by host tests with mocks; no
|
||||
operational unsupported stepping path is exposed.
|
||||
- Host tests (Astra runs them): rejection-before-mutation, band geometry,
|
||||
derived centres, fail-closed behaviour under an injected upload/sync
|
||||
failure, stationary packing byte-identical, execution guard (positive-step
|
||||
run with a moving body raises before upload/launch; stationary unaffected). No kernels, no GPU runs, no
|
||||
y-only VIV oscillator (S4), no force RHS.
|
||||
|
||||
## 10a. S2a — fixed-centre moving links (implemented on `claude/moving-bodies-s2a`)
|
||||
|
||||
- `MovingCircleLinks` (`kernels/step/moving_circle.cu`, dense double buffer)
|
||||
runs before the static curved kernel when moving tables are attached: one
|
||||
thread per (band cell, direction) slot, FP64 `{cx, cy, r}` at the applied
|
||||
offset, q/hit/clamp/fallback in the order of `build_cut_links` with explicit
|
||||
`_rn` operations (host/device bit parity is a test target, not a claim),
|
||||
then the unchanged `apply_bouzidi_link` with spin wall velocity only.
|
||||
- `obs` keeps its contract (CME and torque from `apply_bouzidi_link`). A
|
||||
separate per-body diagnostics buffer receives `cme`, the Wen term
|
||||
`u_w * dm`, `link_dm` and the link count from the same link outputs; it is
|
||||
zeroed with obs and only exists when moving circles do. Optional
|
||||
`slot_debug` (q, fallback, rx, ry per slot) is null in production.
|
||||
- Displacement is disabled: `set_body(offset=..., vx=..., vy=...)` on a
|
||||
translating circle raises before mutation, before and after initialize, so
|
||||
no pending target can exist and `run`/`step`/direct `LBMStepper.step` all
|
||||
hold at the applied geometry. `initialize()` never reads or consumes
|
||||
`pending`. `stepper.blocked` is cleared only when the moving tables are
|
||||
attached (initialize, stepper recreation).
|
||||
- S3 rule (not implemented): once targets are re-enabled, any pending target
|
||||
must raise in `run`/`step` and in `LBMStepper.step` alike before any launch,
|
||||
swap or step-count change; zero-step calls never consume it.
|
||||
- Not validation: a pass of the zero-motion gates says nothing about moving
|
||||
geometry. GPU parity (G0a/G0b) is unverified until run on a free device.
|
||||
|
||||
## 10b. S3a — translation with refill (implemented on `claude/moving-bodies-s3a`)
|
||||
|
||||
- Targets are enabled again (4a). `set_body(offset, vx, vy)` validates against
|
||||
`applied`; a pending target blocks direct `LBMStepper.step(n > 0)` before any
|
||||
launch; `run(n > 1)` with a target raises before mutation; `run(0)` never
|
||||
consumes it; `run(1)` advances it.
|
||||
- Per advancing step, on one stream: upload the immutable FP64 records
|
||||
`{c_old, c_new, r, u}` (only when changed) → M1 `MovingCircleCells` → M2
|
||||
`MovingCircleLinks` (`u_w = u + omega x r` at the hit point) → static curved →
|
||||
`OneStep` → swap → force region → sensors → synchronize → read `no_donor` →
|
||||
commit (`applied := pending.offset`) → upload the hold record
|
||||
(`c_old = c_new = applied`, `u = 0`) → synchronize. Hold steps launch no M1
|
||||
and are the S2a path. Commit point: the advancing step-group sync plus a
|
||||
zero `no_donor` count; a failure before it commits nothing. The hold-record
|
||||
upload stages the next step after the commit: if it (or its sync) fails,
|
||||
`applied` already matches the moved device geometry, but the Simulation is
|
||||
still marked failed and refuses reuse. Any failure from the first upload on
|
||||
marks the circles and the Simulation failed.
|
||||
- M1 classifies with the old/new predicates only (no flag reads). Covered
|
||||
cells add raw `sum c G` / `sum G` of `G^{n-1}` to `cover_*` and become
|
||||
`SOLID|OBSTACLE|BC_CURVED`. Uncovered cells: donors are fluid in both
|
||||
geometries; `rho_bar` over all of them; NEQ from the donor with the largest
|
||||
unnormalized `c·n > 0` (first direction wins ties, `n` from `c_new`);
|
||||
`u_w` (translation + spin) at the cell position; all nine slots written;
|
||||
raw `sum c G`, `sum G` of the refill go to `uncover_*`; no donor →
|
||||
`f_eq(RHO, u_w)` plus `no_donor` (hard failure before commit); no NEQ donor
|
||||
→ `eq_only`.
|
||||
- `get_flags()` reads the device flags back (synchronized) when moving tables
|
||||
exist; otherwise it returns the host array as before.
|
||||
- Not selected: the structural force. CME+node vs CME+Wen stay OPEN until X1
|
||||
and G4 have run.
|
||||
|
||||
## 11. Remaining decisions
|
||||
|
||||
Structural RHS (section 6); motion-record layout; refill details; coupling
|
||||
scheme; benchmark definitions and digitized reference data; tolerances;
|
||||
execution/resource feasibility (Astra, pending); whether a quiescent domain
|
||||
(zero inlet velocity) runs cleanly in the current config.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Moving-boundary force spikes: evidence and source index
|
||||
|
||||
Updated 2026-10-31.
|
||||
|
||||
This note indexes supplied audited observations and primary-source extractions.
|
||||
It does not report a newly measured cure or implement an alternative method.
|
||||
Paper details below were supplied by the parent investigation; no new source
|
||||
retrieval or independent paper verification was performed for this note.
|
||||
|
||||
## Current implementation and interpretation
|
||||
|
||||
- Celeris M1 reconstructs a fresh node as `feq(rho_bar, u_wall) + donor NEQ`.
|
||||
- In exact arithmetic, the donor non-equilibrium component has zero mass and
|
||||
momentum when formed against the donor's own equilibrium moments.
|
||||
- Re-projecting that same reconstruction onto the same wall velocity therefore
|
||||
does not, by itself, change its mass or momentum or supply a cure for spikes.
|
||||
- Floating-point residuals should be measured separately from this algebraic fact.
|
||||
- Peng's alternative differs upstream: extrapolate PDFs from three donors, then
|
||||
replace selected moments. It is not simply another projection of current M1.
|
||||
- PPO interval-force observations and native per-step FSI right-hand-side forces
|
||||
are distinct signals; comparisons must identify which signal is being used.
|
||||
|
||||
## Audited run observations supplied to this note
|
||||
|
||||
- D40 revision: `e9213dd2cc9cfed0e8487a3cf4ebdd94d23dc64d`.
|
||||
- Window: first `97.303 D/U` of physical time.
|
||||
- Maximum displacement: `max |y|/D = 0.304893`.
|
||||
- Maximum native force-coefficient increment: `max |ΔCy| = 0.66688`.
|
||||
- D20 over the same physical-time window: `max |ΔCy| = 1.80735`.
|
||||
- The largest D40 event involved 30 nodes.
|
||||
- These are supplied audited observations, not measurements made for this note.
|
||||
- Time steps and trajectories differ between runs. This comparison establishes
|
||||
neither spatial convergence nor a unique cause for the force spikes.
|
||||
|
||||
Evidence locations supplied by the investigation:
|
||||
|
||||
- Horsea: `/tmp/celeris-results-pinball-resolution`.
|
||||
- Mac: `validation-evidence/pinball-force-diagnosis`.
|
||||
|
||||
These locations index run evidence, not local copies of the cited papers.
|
||||
|
||||
## Tao, Hu, and Guo (2016): time-step decoupling
|
||||
|
||||
Computers & Fluids **133**, 1–14. DOI:
|
||||
[10.1016/j.compfluid.2016.04.009](https://doi.org/10.1016/j.compfluid.2016.04.009).
|
||||
[Publisher abstract](https://www.sciencedirect.com/science/article/pii/S0045793016301116).
|
||||
|
||||
- Abstract-level support only: non-equilibrium refill reduces oscillations.
|
||||
- With local iteration, the compared refill methods give similar oscillation magnitudes.
|
||||
- Time-step decoupling reduces oscillations further in the reported sedimentation,
|
||||
Couette, and Poiseuille examples.
|
||||
- The exact decoupling schedule and time-step ratios have not been verified.
|
||||
- This abstract does not establish equivalence to an arbitrary force filter.
|
||||
- Do not implement a claimed reproduction until the actual methods are available.
|
||||
|
||||
## Marson et al. (2021): local iterations
|
||||
|
||||
Physical Review E **103**, 053308. DOI:
|
||||
[10.1103/PhysRevE.103.053308](https://doi.org/10.1103/PhysRevE.103.053308).
|
||||
[Open repository record](https://archive-ouverte.unige.ch/unige:151868).
|
||||
|
||||
- §VI, p. 18 discusses pressure waves associated with fresh nodes.
|
||||
- Appendix E.3, p. 25 and Fig. 18, p. 24 describe the local procedure.
|
||||
- Copy a local sublattice, perform local collision and streaming, write the fresh
|
||||
nodes, and repeat before the global collision and streaming operation.
|
||||
- One to three iterations performed best in the reported tests; this is not a
|
||||
universal iteration-count recommendation.
|
||||
- The reported smoothed torque-noise assessment is qualitative here.
|
||||
- Its thin-shell setting differs from the current moving-body configuration.
|
||||
|
||||
## Peng (2016): extrapolation and moment replacement
|
||||
|
||||
Computers & Mathematics with Applications **72**, 349–374. DOI:
|
||||
[10.1016/j.camwa.2015.08.027](https://doi.org/10.1016/j.camwa.2015.08.027).
|
||||
|
||||
- §5.1.2, Eq. (50), p. 368: extrapolate PDFs along an outward normal direction:
|
||||
`f_new(x) = 3 f(x + e) - 3 f(x + 2e) + f(x + 3e)`.
|
||||
- §5.1.3, Eq. (51): wall equilibrium plus donor non-equilibrium is the family
|
||||
containing the current M1 reconstruction.
|
||||
- §5.1.4, Eqs. (52)–(54): in MRT space, replace momentum with `j = rho0 u_wall`,
|
||||
preserve the other moments, then apply the inverse transform.
|
||||
- A compressible Celeris adaptation must choose the appropriate density;
|
||||
copying the paper's `rho0` blindly is not justified.
|
||||
- Three valid donors require new eligibility checks and fallback behavior beyond
|
||||
the current one-link shared-body clearance rule.
|
||||
- §§5.2–5.3 retain nonzero fluctuations in specific configurations; these results
|
||||
do not promise elimination of force spikes in the present problem.
|
||||
|
||||
## Bounded next investigation
|
||||
|
||||
1. Retain current M1 as the baseline and compare matched prescribed trajectories
|
||||
over matched physical-time windows.
|
||||
2. Record raw force RHS, node crossings, donor moments, mass and momentum changes,
|
||||
impulse, spectra, and trajectory response together.
|
||||
3. Evaluate local iterations, Peng-style reconstruction, and force filtering as
|
||||
separate interventions, with explicit definitions and diagnostics.
|
||||
4. Obtain the actual time-step-decoupling methods before implementing that method.
|
||||
|
||||
The current task indexes evidence only; it makes no alternative-method change.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Opt-in shifted blocked candidate
|
||||
|
||||
This candidate is based on fbe7377. The default remains unshifted. Enable with
|
||||
`LBMConfig(ddf_shifting=True)` only on the supported routes below.
|
||||
|
||||
| Route | Candidate support |
|
||||
|---|---|
|
||||
| PeriodicBlockStepper, FP32 D2Q9 SRT double-buffer, no LES | shifted or unshifted |
|
||||
| FixedPatchStepper, every level blocked and same representation | shifted or unshifted |
|
||||
| Passive stationary circular BFL boundary and raw exchange | supported candidate scope |
|
||||
| Synchronized three-level child remap | private raw shifted snapshot, exact overlap/copy |
|
||||
| Dense root/refinement or mixed representation | rejected for shifted |
|
||||
| Slot/reservoir, surface actuation, velocity Jet | existing public shifted exclusion retained |
|
||||
| Other collision/lattice/storage/streaming/LES combinations | existing exclusions retained |
|
||||
|
||||
The D2Q9 equilibrium helper accepts a retained density offset only in shifted
|
||||
compilation. BlockStep computes delta=sum(g) before adding the unit density;
|
||||
velocity uses the existing macro calculation. The existing SRT collision and
|
||||
all unshifted expressions remain. No global precision type or duplicate
|
||||
collision implementation is introduced. Ordinary shifted Simulation callers
|
||||
still use the existing rho−1 overload; this candidate does not change their
|
||||
numerical method.
|
||||
|
||||
Public initialize receives physical populations and performs shifted subtraction
|
||||
in FP64 before the one FP32 storage cast. Public shifted download returns FP64
|
||||
physical populations with exact-rational D2Q9 background weights. Internal raw
|
||||
snapshots retain FP32 g and a representation tag; runtime remap never decodes
|
||||
and re-encodes overlap. Exact mass is interpreted as 1+sum(g), and momentum as
|
||||
sum(c*g); analyses should use raw values for tiny perturbation budgets. The
|
||||
FP32 BFL force constants have their own arithmetic rounding; no exact global
|
||||
conservation claim follows from shifting.
|
||||
|
||||
Planar helper formulas include their shifted reference-density offsets and have
|
||||
actual-header host tests. Their public shifted route remains excluded until a
|
||||
separate test-gated migration. Next velocity-Jet work must validate physical
|
||||
wall density, incoming/outgoing populations, raw force/mass, ownership and
|
||||
transfer interactions before relaxing that restriction.
|
||||
|
||||
Evidence so far: selected-cell CUDA replay favored retained-delta shifting;
|
||||
this is not full-solver validation. Host algebra/storage/remap tests and staged
|
||||
GPU continuation, three-grid cylinder physics, raw budgets, Couette helper
|
||||
stress and measured runtime are separate gates. Original failed experiments
|
||||
remain evidence; no acceptance threshold is relaxed.
|
||||
|
||||
## Direct-input-momentum equilibrium candidate
|
||||
|
||||
The shifted BlockStep now constructs the linear equilibrium term from input
|
||||
momentum j, without reconstructing rho*u. For opposite axis pairs the odd term
|
||||
is j/3; for diagonal pairs it is (jx +/- jy)/12, evaluated with explicit
|
||||
round-to-nearest division. The even term is
|
||||
w*[delta + 4.5*(c dot j)*(c dot u) - 1.5*(j dot u)].
|
||||
This is the same second-order D2Q9 equilibrium in exact arithmetic. Original SRT,
|
||||
storage, boundaries, and transfers are unchanged. No collision residual or force
|
||||
is corrected afterward. Existing four/five-argument equilibrium APIs are retained;
|
||||
only shifted blocked collision calls the new six-argument momentum helper.
|
||||
|
||||
The all-fluid read-only CUDA replay reduced signed x-momentum error by 120–2285x
|
||||
at five sampled states, with lower mass/momentum and second-moment RMS errors.
|
||||
Signed mass bias was larger (~1e-12/cell/hop), so full evolving mass budgets remain
|
||||
mandatory. This local evidence does not establish physical convergence or speed.
|
||||
The unshifted default and its arithmetic path remain unchanged.
|
||||
@@ -1,150 +0,0 @@
|
||||
# Uncontrolled steady pinball: development protocol
|
||||
|
||||
Frozen before results: 1280 x 512 D2Q9/FP32/MRT, unshifted double buffer;
|
||||
rho=1, mean U=.01, nu=.004, D=20, Re_D=50. Centres are (600,255.5),
|
||||
(626,270.5), (626,240.5), radius10. No upstream cylinder. Passive probes stay
|
||||
at (800,295.5), (800,255.5), (800,215.5), radius5. Every omega is zero.
|
||||
Parabolic inlet, regularized NEQ damping1; y bounce-back planes .5/510.5;
|
||||
NEQ-extrapolated outlet with backflow clamp (the configured blend is inactive in
|
||||
this MRT route). Initial fluid is
|
||||
parabolic equilibrium. Fixed warmup is 4 NX/U=512000 steps.
|
||||
|
||||
Geometry and fluid parameters follow DynamisLab 609116ec26f19766f5e9ce2b0df427d4e24faae8,
|
||||
`legacy_test/core/legacy_env_builder.py:build_steady_cloak` and the retained
|
||||
`config_lbm_pinball_legacy_re50d_u001.json`. This is a modern Celeris reconstruction,
|
||||
not a bitwise Legacy reproduction: Legacy uses shifted incompressible MRT, a
|
||||
different curved interpolation and prescribed parabolic outlet velocity. Current
|
||||
Celeris uses standard MRT, two-branch BFL and extrapolated outlet velocity.
|
||||
The fixed-hold control uses the SAME modern settings as the free case.
|
||||
|
||||
## Shared translation and ownership
|
||||
|
||||
`Translation(..., shared=True)` is explicitly transverse only. Reusing one
|
||||
envelope INSTANCE declares common motion; equal but separate envelopes are not
|
||||
linked. Before initialization and every positive Simulation.run, group members
|
||||
must have identical applied offsets, effective target offsets and interval wall
|
||||
velocities. Stage each member with set_body, then call run(1) once. An incomplete
|
||||
or inconsistent target group is rejected before device uploads and can be fixed.
|
||||
Existing pending-target direct-stepper restrictions remain.
|
||||
|
||||
Only members of that group may have overlapping whole swept bands. Their fixed
|
||||
surface clearance must exceed4 lattice cells. This conservative bound exceeds
|
||||
2 sqrt(2)+MAX_STEP = 3.329: M1 changed cells lie in old/new disks and read one-link
|
||||
neighbors; M2 reads at most two links outside its new disk. Old/new relative
|
||||
displacement is at most .5. Thus reads cannot reach another member's old/new
|
||||
solid or M1 write region. M1 writes are disjoint. M2 writes opposite population
|
||||
slots inside its own new disk, also disjoint. Separate first_slot ranges still
|
||||
allocate each body's logical band/debug storage. Shared band cells alone do not
|
||||
write anything: M1 returns unless membership changes; M2 returns unless a cut
|
||||
link exists. Unrelated moving/static body and probe overlaps still reject.
|
||||
|
||||
This is a sufficient restricted-group condition, not support for near-contact
|
||||
or independent motion. The r4, distance8.3, dy=.5 counterexample remains rejected.
|
||||
CPU tests enumerate candidate donors/write footprints over subcell phases;
|
||||
GPU tests compare fixed vs hold and member-order permutations through crossings
|
||||
at wall speed .01 over the full +/-D envelope (8000 steps). The initial .5-speed
|
||||
fixture failed with NaNs; a single-circle control also failed at step69. That
|
||||
Mach~.866 fixture was unsuitable as a low-Mach numerical gate. Its failure is
|
||||
retained; the geometric MAX_STEP proof is unchanged, not a stability guarantee.
|
||||
CUDA kernels and arithmetic are unchanged.
|
||||
|
||||
## New structural parameters (not inherited from the paper)
|
||||
|
||||
One transverse state y,v; total assembly M=10 rho (3 pi D^2/4)=9424.77796,
|
||||
fn=U/(5D)=.0001, Tn=10000, zeta=.01, K=M(2 pi fn)^2,
|
||||
C=2 zeta M(2 pi fn). Initial y=v=0, no seed or later kick. Force is the sum
|
||||
of the three instantaneous CME_y+Wen_y values from the previous fluid step;
|
||||
node impulses are logged separately and never added. The damped oscillator is
|
||||
integrated exactly for that piecewise-constant force over dt=1. Wall velocity
|
||||
is y[n+1]-y[n], not the endpoint oscillator velocity. No filtering or retuning.
|
||||
|
||||
Observe600000 steps=60Tn=300 D/U after warmup, in each of free and fixed-hold
|
||||
cases. Final20Tn is a descriptive comparison window; ten-Tn blocks expose drift.
|
||||
No settled/periodic claim follows merely from reaching the cap. Stop on |y|>D,
|
||||
nonfinite values, no_donor, rejected displacement or speed>.05. Report any quiet
|
||||
response honestly. No automatic extension. --smoke uses32 warmup+32 steps only.
|
||||
|
||||
Native-step rows retain y,v, each body's CME/Wen/node terms and refill counters;
|
||||
each600000x15 FP64 file is72MB. No full-field output or policy training. Compile
|
||||
and run on one device serially, keep original outputs and dirty trees untouched.
|
||||
This stage makes no claim of physical grid convergence or vibration suppression.
|
||||
|
||||
Run from checkout with PYTHONPATH=src and the existing Python environment:
|
||||
`python research/steady_pinball/run.py --smoke --out <new-external-directory>`
|
||||
then separately `--out <new-free-directory>` and `--fixed --out <new-fixed-directory>`.
|
||||
|
||||
## Zero-damping development follow-up (predeclared 2026-10-31)
|
||||
|
||||
The original d0bcefa run and its zeta=.01 results remain historical evidence.
|
||||
The current runner exposes three fixed `--case` choices, all with C=0 as requested:
|
||||
|
||||
| Case | M / (rho 3 pi D^2/4) | fn | M | K | Purpose |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| Z0 | 10 | .0001 | 9424.77796 | .00372075320 | Remove damping only |
|
||||
| M2 | 2 | .0001 | 1884.95559 | .000744150640 | Reduce mass and stiffness together at fixed fn |
|
||||
| R2 | 2 | .00006 | 1884.95559 | .000267894231 | Change stiffness only relative to M2 |
|
||||
|
||||
R2 deliberately places fn at the retained fixed-body lift peak, f=.00006
|
||||
(St=.12 on mean U). This is an informed development choice, not independent
|
||||
physical validation or a promise of lock-in. All geometries, fluid settings,
|
||||
512000-step fixed warmup, y0=v0=0, force time level and unfiltered RHS are unchanged.
|
||||
No run result may select a replacement parameter or an automatic extension.
|
||||
|
||||
Each case has a 600000-step cap, the same 300 D/U physical duration. This equals
|
||||
60 natural periods for Z0/M2 and 36 for R2. Compare the common final200000 native
|
||||
steps, and six100000-step blocks, retaining actual natural/response frequencies
|
||||
and ongoing drift. Do not call those blocks ten R2 periods. No settled-VIV claim
|
||||
follows from a larger displacement. Report all three outcomes, including quiet
|
||||
responses or stopped cases, alongside the existing fixed and damped-free records.
|
||||
|
||||
Unchanged stops: |y|>D, invalid move/speed, nonfinite values or no_donor. A 1800s
|
||||
per-case wall cap and GPU headroom check bound resource use. Preserve partial rows;
|
||||
do not add damping, clip trajectories, filter forces, or relax any stop. Each
|
||||
case adds72MB of scalar history, no full fields. The separate Legacy/modern
|
||||
baseline diagnosis does not change this FSI protocol.
|
||||
|
||||
## Matched D40 resolution comparison (predeclared 2026-10-31)
|
||||
|
||||
The first finer case is M2, with exactly the same mass ratio2, reduced velocity5,
|
||||
zero structural damping, initial rest state and physical geometry as the retained
|
||||
D20 M2. Targeting a development response of0.5--1D does not imply this case will
|
||||
reach it. Do not change the structure within this comparison or discard a smaller
|
||||
response. D60 is a supported scaling choice, not an automatically scheduled run.
|
||||
|
||||
For s=D/20 use dx'=dx/s, dt'=dt/s^2, U'=.01/s, nu'=.004, rho'=1,
|
||||
fn'=fn/s^2, M'=s^2 M and K'=K/s^2. The same physical force per unit span
|
||||
has F'_lat=F_lat/s. For D40 this gives U=.005, fn=.000025,
|
||||
M=7539.82237, K=.000186037660, Re_D=50, and Tn=40000 steps.
|
||||
The y walls are at .5 and ny-1.5, while x inlet/outlet source nodes are at0
|
||||
and nx-1. Therefore map x'=s*x and y'=.5+s*(y-.5), with
|
||||
nx'=s*(1280-1)+1, ny'=s*(512-2)+2. D40 uses2559x1022 and centres
|
||||
(1200,510.5),(1252,540.5),(1252,480.5); every radius, probe and envelope scales
|
||||
with D. The parabolic profile retains the same normalized wall coordinate.
|
||||
This preserves the existing ghost-source boundary convention, without asserting
|
||||
that it is an exact continuum open-boundary location.
|
||||
|
||||
Warmup=2048000 and response=2400000 native steps at D40: the SAME256 and300
|
||||
D/U durations as D20, including60 natural periods after release. Compare the
|
||||
six50-D/U blocks and the common final100-D/U window. Report max absolute y/D,
|
||||
half-range, RMS, frequency and drift; peak-to-peak is not amplitude. No settling
|
||||
claim follows from completion. Keep raw individual CME/Wen/node terms and all
|
||||
crossing/fallback counters. Normalize net force by .5*rho*U^2*D in each case.
|
||||
Report native crossing/non-crossing force increments separately, plus D40
|
||||
four-step block means aligned with D20 intervals (diagnostic only, never replace
|
||||
the raw spikes). Report final-window high-frequency variance above f*D/U=1,
|
||||
using demeaned FFT Parseval weights, with the same physical cutoff for both cases.
|
||||
Different motion histories confound spike-size comparisons; this is simultaneous
|
||||
space/time/Mach refinement, not proof of a unique coarse-grid cause.
|
||||
|
||||
Only one full D40 M2 is initially scheduled, after CPU checks and a32+32-step
|
||||
smoke. Stop on the unchanged geometry/speed/nonfinite/no_donor checks or a7200s
|
||||
whole-case wall cap; save completed rows and status. The velocity cap scales to
|
||||
.025 so its physical bound is unchanged. No force filtering, trajectory clipping,
|
||||
automatic parameter search or extension. Scalar history is288MB; no full fields
|
||||
or DDF snapshots are written. Two FP32 population buffers require about188MiB,
|
||||
before flags, moving tables and context overhead; measure actual peak usage in
|
||||
the smoke and full run. Keep all D20 outcomes. No production source changes.
|
||||
|
||||
Commands (one GPU and serial compilation, existing environment):
|
||||
`python research/steady_pinball/run.py --case M2 --D 40 --smoke --out <new-smoke-dir>`
|
||||
then `python research/steady_pinball/run.py --case M2 --D 40 --wall-seconds 7200 --out <new-full-dir>`.
|
||||
@@ -1,96 +0,0 @@
|
||||
# Supported settings: 2D Jet + fixed multilevel refinement (candidate 888c8038)
|
||||
|
||||
Every entry names the guard that enforces it (file and function or kernel guard,
|
||||
at source revision 888c80381c006e976be2af8a452288729fc17c1b) and, where one
|
||||
exists, an existing test that exercises it. A setting not listed here is not
|
||||
claimed. Observations at the end are tied to the exact case in which they were
|
||||
measured; they are not general bounds.
|
||||
|
||||
## Lattice, storage, collision
|
||||
|
||||
| Setting | Supported | Guard |
|
||||
|---|---|---|
|
||||
| Dimension / lattice | 2D D2Q9 only (3D refinement is future work) | `lbm/refinement.py` `FixedPatch2D.from_config`; `#error` guards in `lbm/kernels/blocked.cu` |
|
||||
| Precision / streaming | FP32 storage, double buffer | same guards |
|
||||
| Collision on refinement levels | SRT or MRT; TRT and LES rejected | `lbm/refinement.py` `FixedPatch2D.from_config` (re-checked for fine boundaries in `prepare_fine_boundary`) |
|
||||
| Shifted (ddf_shifting) storage | supported (every level is blocked); one representation on all levels | `lbm/refinement_stepper.py` `FixedPatchStepper._check_storage` |
|
||||
| Fine relaxation | 1/tau_fine must lie inside the configured omega guards | `lbm/refinement.py` `FixedPatch2D.from_config` |
|
||||
| Single blocked level `FixedPatchStepper(refinements=[], level_blocks=[(by, bx)], level_objects=[circles])` (replaces `PeriodicBlockStepper`, removed in 8e1feff) | doubly periodic, SRT only, no LES/Jets/projection/exchange observation, stationary passive circles; `download()` returns a 1-tuple | `lbm/refinement_stepper.py` `_check_levels`, `FixedPatchStepper.__init__`; `lbm/blocks.py` `static_circle_tables` |
|
||||
| Refinement storage | blocked on every level: `level_blocks` is required, one tile `(by, bx)` per level dividing that level; a whole-level tile (`(ny, nx)` at the root, the fine window shape for one rectangle) always divides. Dense and mixed dense/blocked storage were removed (parity of D/D, D/B and B/B recorded at 1573942 and 2469af7) | `lbm/refinement_stepper.py` `_check_levels`, `FixedPatchStepper.__init__` |
|
||||
|
||||
Tests: `tests/unit/test_refinement_mrt.py`, `tests/validation/multigrid/test_refinement_mrt_gpu.py`,
|
||||
`tests/unit/test_shifted_storage.py`, `tests/unit/test_shifted_block_operators.py`.
|
||||
|
||||
## Refinement hierarchy (`FixedPatchStepper(cfg, *, context, refinements, ...)`)
|
||||
|
||||
| Setting | Supported | Guard |
|
||||
|---|---|---|
|
||||
| Depth | at least one refinement level; one entry per level in `level_blocks`/`level_objects`/`level_jets` | `lbm/refinement_stepper.py` `_check_levels` |
|
||||
| Single L1 rectangle | two-coarse-cell margin to the domain edge | `lbm/refinement.py` `FixedPatch2D.from_config` |
|
||||
| Several L1 rectangles (union) | require `level_blocks[0]` and `level_blocks[1]`; transfers must close (dependency checks, no fixed gap rule) | `lbm/refinement_stepper.py` `_check_levels`; `lbm/refinement.py` `_check_parent_pull_closure` |
|
||||
| Nested levels | depth >= 3 must be all blocked; a blocked level requires blocked levels below; a scalar child needs a scalar parent | `lbm/refinement_stepper.py` `_check_levels`; `lbm/hierarchy.py` `build_hierarchy` |
|
||||
| Nested transfer support | child only over inherited ACTIVE cells, donors/receivers uncovered ACTIVE, no child over the parent restriction footprint | `lbm/refinement.py` `_nested_kind` |
|
||||
| Tile shape | positive `(by, bx)`; the level allocation must be divisible by the tile (no partial tiles yet) | `lbm/refinement_stepper.py` `_check_levels`, `lbm/refinement.py` `FixedPatchSet2D.fine_layout` (shape); `lbm/blocks.py` `BlockLayout2D.build` (divisibility) |
|
||||
| Coarse boundary | `periodic`, `slot_reservoir` or `channel` | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__` |
|
||||
| `slot_reservoir` | on-site velocity bottom slot, reservoir top, velocity 0, 2 <= slot width < nx, patch rows 4..ny-4 | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__`; `lbm/refinement.py` `prepare_fine_boundary` |
|
||||
| `channel` | all levels blocked; Zou-He inlet, pressure-NEQ outlet, stationary bounce-back walls; transfers away from the closure | `lbm/channel.py` `validate_channel_config`, `validate_channel_transfers` |
|
||||
| Explosion | `constant` (default); `projected_linear` only periodic, all-blocked SRT, no bodies/Jets, no LES (experimental) | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__` |
|
||||
| L2 runtime remap (`remap_finest`) | periodic three-level L2 sets only, no L0/L1 bodies, identical pinned boundary | `lbm/refinement_stepper.py` `FixedPatchStepper.remap_finest` |
|
||||
|
||||
Tests: `tests/unit/test_refinement_three_level_multipatch.py`, `tests/validation/multigrid/test_refinement_multipatch.py`,
|
||||
`test_refinement_three_level.py`, `test_refinement_three_level_multipatch_gpu.py`, `test_refinement_blocked_base.py`,
|
||||
`test_refinement_blocked_fine.py`, `test_channel_root_gpu.py`, `tests/unit/test_channel_root.py`,
|
||||
`tests/unit/test_projected_pair_stepper.py`, `test_runtime_child_remap_gpu.py`, `test_pinned_body_remap_gpu.py`.
|
||||
|
||||
## Bodies and Jets on refined levels
|
||||
|
||||
| Setting | Supported | Guard |
|
||||
|---|---|---|
|
||||
| Bodies | stationary circles without sensor/force-region roles, entirely inside the patch union, fully resolved, disjoint, stencil away from the two-hop transfer footprint | `lbm/refinement.py` `prepare_fine_boundary` |
|
||||
| Level | not on L0; only on the finest level of a nested hierarchy | `lbm/refinement_stepper.py` `_check_levels` |
|
||||
| Jet condition | velocity only; `pressure` is reserved and raises NotImplementedError | `body/jets.py` `Jet.__post_init__` |
|
||||
| Jet geometry | each Jet selects resolved links of exactly one stationary body; no Jet/Jet or Jet/SurfaceActuation overlap; abs(amplitude) < 1 lattice | `body/jets.py` `Jet.__post_init__`, `jet_link_shapes`, `_surface_links` |
|
||||
| Jets with shifted storage | only in the all-blocked stepper with SRT and no LES | `body/jets.py` `_surface_links` |
|
||||
| SurfaceActuation | unshifted storage only | `body/objects.py` `SimObject.validate_surface_actuation` |
|
||||
| Raw boundary exchange (force) | blocked body levels only; the window ledger requires `observe_raw_exchange` | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__` |
|
||||
| Checkpoint with Jets | refused | `simulation.py` `Simulation.save_checkpoint` |
|
||||
|
||||
Raw exchange is the cut-link momentum reaction (wall + aperture), not a validated
|
||||
nozzle thrust or stress load. Jet amplitude is a lattice velocity, not a flow rate.
|
||||
|
||||
Tests: `tests/unit/test_jets.py`, `test_jets_review.py`, `test_shifted_jets.py`, `test_raw_exchange.py`,
|
||||
`tests/validation/multigrid/test_refinement_jets.py`, `test_shifted_jets_gpu.py`, `test_raw_exchange_gpu.py`,
|
||||
`test_exchange_window.py`, `test_fine_surface_gpu.py`.
|
||||
|
||||
## Observations from specific tested cases (not general bounds)
|
||||
|
||||
- SRT channel root vs uniform OneStep, strict-parity test
|
||||
`tests/validation/multigrid/test_channel_root_gpu.py::ChannelRootGPU::test_uniform_matched_root`:
|
||||
in one of its 18 sub-cases (2 storage modes x 3 profile/clamp/damp settings x 3 blocks; which
|
||||
one is not recorded), 1 of the 4320 compared values differs, max abs 2.33e-10, max rel 1.77e-7
|
||||
(about 2 ULP). The test remains failed; its cause and any general bound are unresolved.
|
||||
- MRT root parity test
|
||||
`tests/validation/multigrid/test_refinement_mrt_gpu.py::RefinementMRTGPU::test_uniform_matched_mrt_root`
|
||||
(12 sub-cases: 2 storage modes x 2 profiles x 3 blocks, default outlet settings) passed bitwise
|
||||
in the Horsea focused GPU run at K1 7813334 (2026-10-21).
|
||||
- Periodic Taylor-Green Re 100, measured at 36ffc8b1 (V3 matched pair, N128 depth 2):
|
||||
every per-sample screen passed; the constant explosion's aggregate worst velocity L2,
|
||||
0.004418, exceeds its preregistered 0.002 budget, while projected_linear passed that budget.
|
||||
- Curved-wall pressure (pilot harness, exact-q analytic annulus, tau 0.8): the coarse swirl r1
|
||||
case exceeded its terminal density-gain cap (1.105e-4 vs 1e-4), so the frozen 9-case matrix
|
||||
is NOT_ESTABLISHED. No validated fix exists. This is a pilot harness, not production code.
|
||||
- DFG 2D-1 Re 20 (V4, SRT, t* 60, pressure-NEQ outlet and local Zou-He inlet): refined and uniform
|
||||
runs were NOT_STEADY under the frozen steady rule, so no interval verdict was given.
|
||||
|
||||
## Moving circles (dense `Simulation`, uniform grid only)
|
||||
|
||||
Translating circles with optional spin are supported on the dense double
|
||||
buffer D2Q9 FP32 path only (not on `FixedPatchStepper` or refined levels).
|
||||
API, supported/rejected modes and limits: `docs/moving_bodies.md`; stage-1
|
||||
evidence: `docs/validation/moving_bodies_stage1.md`.
|
||||
|
||||
## Not supported in this release
|
||||
|
||||
3D, dynamic/automatic AMR (tagging, time-varying hierarchies beyond `remap_finest`),
|
||||
moving bodies on refined levels, pressure Jets, Jet checkpoints, LES or TRT on refined
|
||||
levels, partial (non-dividing) tiles.
|
||||
@@ -1,15 +1,11 @@
|
||||
# Synchronized leaf G remap and manual continuation
|
||||
|
||||
This is a CPU state-transfer component, **not a runtime adaptive solver**.
|
||||
It changes only an L2 rectangle union in a three-level periodic hierarchy.
|
||||
The body-free case is shown first; stationary L2 bodies/Jets follow the bounded
|
||||
support contract below. L0/L1 topology, relaxation parameters, direction order,
|
||||
It changes only a separated L2 rectangle set in a three-level, body-free,
|
||||
periodic hierarchy. L0/L1 topology, relaxation parameters, direction order,
|
||||
physical frame and completed time remain unchanged. Existing geometry checks
|
||||
validate child support/nesting and actual transfer dependencies, without a fixed
|
||||
rectangle-gap rule. The three-level remap limit is unchanged by the stepper's
|
||||
arbitrary-depth level-list support. The CPU helper itself adds no CUDA,
|
||||
scheduler, allocator, boundary handling, collision or ghost refill. A bounded
|
||||
runtime adapter is described below.
|
||||
validate child support/nesting and separation. The CPU helper itself adds no CUDA, scheduler, allocator, boundary handling,
|
||||
collision or ghost refill. A bounded runtime adapter is described below.
|
||||
|
||||
```python
|
||||
from CelerisLab.lbm.refinement import FixedPatchSet2D
|
||||
@@ -33,13 +29,11 @@ origin is retained. Physical lower edges are `origin + spacing*(x,y)` on L0,
|
||||
`origin + spacing*(parent.fine_origin + (x,y)/4)` on L2.
|
||||
|
||||
Factory options `coarse_boundary` and `objects` must declare `"periodic"` and
|
||||
an empty collection; L2 bodies use `objects` as described below.
|
||||
LBMConfig's channel settings do not select this operator's boundary mode.
|
||||
Geometry metadata is factory-created;
|
||||
an empty body collection (all levels). LBMConfig's channel settings do not
|
||||
select this operator's boundary mode. Geometry metadata is factory-created;
|
||||
manual mutation/forging of metadata is unsupported. Caller certifies the
|
||||
actual state matches the declared boundary and has no forcing, that all device
|
||||
work and old readers are quiescent, and that inputs are the **current owned-leaf
|
||||
postcollision G**.
|
||||
actual state has no bodies/forcing, that all device work and old readers are
|
||||
quiescent, and that inputs are the **current owned-leaf postcollision G**.
|
||||
Count/phase stamps check consistency, not freshness or active-buffer identity.
|
||||
|
||||
`current_g` is three NumPy Q-major arrays `(9,ny,nx)`. For blocked inputs,
|
||||
@@ -77,12 +71,11 @@ PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_synchronized_r
|
||||
|
||||
## Explicit runtime continuation
|
||||
|
||||
`replacement = current.remap_finest(rectangles)` returns a new ready
|
||||
`replacement = current.remap_child(rectangles)` returns a new ready
|
||||
`FixedPatchStepper` at the **same absolute step_count**. It supports only
|
||||
periodic three-level runs with no L0/L1 bodies and an existing L2
|
||||
`FixedPatchSet2D`; stationary L2 bodies/Jets follow the support contract below.
|
||||
Scalar or union L1 parents are supported; scalar `child=` is deliberately
|
||||
rejected. L0/L1, relaxation and block sizes stay fixed.
|
||||
body-free periodic three-level runs whose existing L2 uses
|
||||
`child_rectangles_in_parent`. Scalar or union L1 parents are supported; scalar
|
||||
`child=` is deliberately rejected. L0/L1, relaxation and block sizes stay fixed.
|
||||
No tagger, scheduler, in-place replacement, or general rollback is provided.
|
||||
|
||||
The method holds the stepper lock, validates geometry, synchronizes its stream,
|
||||
@@ -99,7 +92,7 @@ resources; an actual old-device synchronization/read failure marks the old
|
||||
stepper failed, consistent with its existing failure semantics.
|
||||
|
||||
```python
|
||||
replacement = current.remap_finest([(20, 16, 36, 40)])
|
||||
replacement = current.remap_child([(20, 16, 36, 40)])
|
||||
# Explicitly choose the returned continuation only after success.
|
||||
current.close()
|
||||
current = replacement
|
||||
@@ -119,25 +112,24 @@ Per-population conservation is not an advancement target under collision.
|
||||
|
||||
### Bounded pinned-body extension
|
||||
|
||||
`replacement = current.remap_finest(rectangles, objects=original_objects)`
|
||||
also accepts stationary Circles inside the L2 union. Each body's **physical
|
||||
boundary, complete BFL support and current per-link velocity must remain
|
||||
identical**. Rectangles are not pinned: they may be split, merged or resized,
|
||||
and other fluid coverage may change, including within the same connected union,
|
||||
provided that physical support is unchanged. Existing nesting, union transfer
|
||||
dependencies, full-circle containment, reverse cut-link
|
||||
coverage and two-hop BFL transfer-footprint checks still apply. This is manual
|
||||
remapping, not adaptive body tracking or runtime body editing.
|
||||
`replacement = current.remap_child(rectangles, child_objects=original_objects)`
|
||||
also accepts stationary Circles in separated L2 rectangles. Every body’s
|
||||
**complete containing rectangle must remain identical**. Several bodies may
|
||||
share one pinned rectangle; there need not be any fluid-only rectangles.
|
||||
Separated fluid-only rectangles may be added, removed or moved. Existing
|
||||
nesting, four-cell gap, full-circle containment, reverse cut-link coverage and two-hop BFL
|
||||
transfer-footprint checks still apply. This is manual remapping, not adaptive
|
||||
body tracking or runtime body editing.
|
||||
|
||||
Supply all original root edge-frame definitions in their construction order.
|
||||
Do not pass `current.boundaries[2].objects`: those are projected fine-local copies.
|
||||
Do not pass `current.child_boundary.objects`: those are projected fine-local copies.
|
||||
There is no retained mutable original-object registry. Before reading device
|
||||
state, remapping reprojects the supplied definition onto the old topology and
|
||||
compares exact ordered circle centers/radii, flags, physical cut links, body
|
||||
IDs, q, lever arms, hits, BFL support,
|
||||
legacy SurfaceActuation specification and current per-link velocity against
|
||||
the existing boundary. Omission, changed geometry/actuation or changed physical
|
||||
boundary/BFL support fail without device reads or allocations; reordered bodies fail
|
||||
the existing boundary. Omission, changed geometry/actuation and changed body
|
||||
rectangle fail without device reads or allocations; reordered bodies fail
|
||||
too. Root object IDs are still assigned by list position, as in the constructor.
|
||||
|
||||
Jets are taken from the live L2 boundary, including the most recent successful
|
||||
@@ -149,16 +141,12 @@ boundary and velocities must match exactly.
|
||||
typed `FineBoundary2D`; it derives L2 fluid leaves as ACTIVE minus SOLID. No
|
||||
caller mask is accepted. Solids, ghosts and holes are never donors and remain
|
||||
NaN, physical-fluid overlap is exact, and exposed L1 still restricts all four
|
||||
old fluid children. The helper checks old/new physical boundary and velocity
|
||||
equality too. Its immutable `pinned_rectangles` is legacy containment metadata,
|
||||
not a remap eligibility constraint; it may be empty for a seam-spanning body.
|
||||
Scalar-parent window offsets remain part of the coordinate conversion.
|
||||
old fluid children. The helper checks the old/new body pins and boundary too.
|
||||
Its immutable `pinned_rectangles` is a canonical, deduplicated tuple, empty for body-free layouts; rectangle input
|
||||
order does not affect it. Scalar-parent window offsets remain part of the
|
||||
coordinate conversion.
|
||||
|
||||
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`.
|
||||
`tests/unit/test_union_body_support.py` also checks seam-spanning circles,
|
||||
rectangle repartition/resize, unchanged physical support and live-Jet
|
||||
preservation. These host checks do not establish GPU evolution accuracy.
|
||||
GPU declarations:
|
||||
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`. GPU declarations:
|
||||
`tests/validation/multigrid/test_pinned_body_remap_gpu.py`, covering no-op
|
||||
next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation,
|
||||
physical boundary/velocity equality, source independence and absolute counts.
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Bounded 2D implementation and validation scope
|
||||
|
||||
This inventory starts from dev `8403956a9afc0b0bb89265cec374392013d89016` and
|
||||
integrates only the reviewed LES normalization production correction from
|
||||
`95cb6d2b9f43c1bdd46cbf27f03f0dfc0877cfb0`. The merged production bytes match that
|
||||
tested candidate exactly; the scope-test changes are retained from `53fad767`.
|
||||
GPU receipts remain associated with their exact tested commits, not relabeled as
|
||||
a new merged-commit run. Historical source identities and gates remain unchanged.
|
||||
|
||||
## Implementation boundaries
|
||||
|
||||
- Uniform D2Q9 supports SRT, TRT and MRT. An available collision selector does not
|
||||
qualify every wall, inlet/outlet, forcing, storage or LES combination.
|
||||
- Fixed refinement supports SRT/MRT, FP32 and double-buffer streaming; TRT and LES
|
||||
are rejected. Shifted refinement requires all-blocked storage. Refined channel
|
||||
boundaries have narrower configuration guards than uniform boundaries.
|
||||
- Uniform checkpoints support compatible same-method continuation. Changing
|
||||
collision or LES parameters is a different evolution, not exact continuation.
|
||||
Jet checkpoints and refined checkpoints are unsupported; synchronized remapping
|
||||
is not persistent restart.
|
||||
- Keep these conclusions 2D. Local D3Q19 operator tests do not establish a supported
|
||||
3D Simulation runtime.
|
||||
|
||||
## Evidence and remaining gaps
|
||||
|
||||
| Area | Available evidence | Boundary of the conclusion |
|
||||
|---|---|---|
|
||||
| MRT cylinder flow and observation windows | Full matched uniform/refined MRT DFG and the window-ledger campaign passed their frozen periodicity, reference and matched-comparison gates. The window campaign used `185a919`; its MRT/no-LES numerical path remains unchanged. Current dev also contains independently scoped test/documentation changes and the LES correction, which do not extend MRT qualification. | This does not qualify other collision/boundary combinations or every refinement interface. Timing improvements are not additional accuracy evidence. |
|
||||
| Uniform forced channel | Six Poiseuille GPU cases on `8403956`: SRT/TRT/MRT at wall-normal resolutions H16/H32 passed. | This is the tested analytic channel configuration, not open-boundary or all-layout qualification. |
|
||||
| Uniform restart | Ten SRT/MRT checkpoint GPU tests passed on `8403956`; the separate representative TRT same-method double-buffer continuation GPU case also passed (one test). | This covers the tested same-method continuation configurations. No LES, Jet or refined checkpoint claim follows. |
|
||||
| Taylor–Green refinement | Independent topology checks are available. The actual-rate phase GPU regression passed one test across two layouts, with errors 1.584e-7 and 4.500e-9. | The physical interface-accuracy gap remains open. Passing topology and phase checks does not establish convergence or close that accuracy gate. |
|
||||
| Porous Couette and annulus | Prior campaign evidence exists and must remain associated with its original setup and source. | The reconstructed annulus is a validation helper, not qualification of a public boundary-condition API. Do not infer absence of evidence from a missing current rerun. |
|
||||
| LES normalization | The reviewed correction is integrated. Exact `95cb6d2` GPU validation passed all nine observations and frozen gates, with three warm-up/reset records, verified 101-source/import identity, DONE and exit 0. Last two SGS decrement relative errors were 6.276e-4 and 6.124e-5 against the unchanged 0.15 gate. | Scope is unforced uniform D2Q9/SRT, shifted FP32, double-buffer streaming and the tested wall-bounded x-periodic shear. No TRT/MRT LES, forcing, EsoPull, objects, refinement or 3D turbulence qualification follows. |
|
||||
|
||||
Historical uniform SRT cylinder-startup and inlet-rounding findings remain separate
|
||||
from the passed MRT DFG result. Raw exchange/window consistency is a measurement
|
||||
contract, not a general stress or force-accuracy certification. Existing source-pinned
|
||||
runners may intentionally reject a newer checkout; never silently repin old receipts.
|
||||
|
||||
## Remaining work
|
||||
|
||||
The related host checks passed (67 checks); they do not replace the GPU or
|
||||
physical-accuracy evidence above. Preserve the existing SRT checkpoint cases,
|
||||
historical LES manifests and all frozen numerical criteria.
|
||||
|
||||
Continue the TG accuracy investigation using its actual fine-level rates and
|
||||
the established analytic convention. Keep the physical accuracy gate open until
|
||||
the relevant comparison passes. Review this integration independently before
|
||||
publication; no new merged-commit GPU run is claimed here.
|
||||
|
||||
No repeat of the accepted full MRT DFG or Cartesian expansion across unsupported
|
||||
combinations is required by this document. Main integration remains a separate
|
||||
review of complete, bounded feature scope.
|
||||
@@ -1,60 +0,0 @@
|
||||
# DFG 2D-2 Re100 GPU campaign (validation-only)
|
||||
|
||||
## Current experimental source provenance
|
||||
|
||||
This development branch now includes the reviewed AMR-channel source changes originating in `5338864748ac67fe2544487c059ba149ebf916da` and `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. The current full-source manifest pins the latter reviewed source snapshot, including the new channel files. This is an explicit new experiment; neither source review nor host tests transfer historical d79 GPU/flow acceptance to it. The channel GPU gate has not yet run at integration time.
|
||||
|
||||
New run JSON records the source-origin pin plus `checkout_revision` (actual Git HEAD), `checkout_dirty`, and the experimental-source label. The origin pin is not claimed to be the run's checkout commit. Numerical definitions and thresholds are unchanged. Runs require a Git checkout for this runtime identity; source hash checks remain active.
|
||||
|
||||
Historical d79 Re100/Jet execution stays reproducible at `69e9b78de9483a5ff21e2d0c74311d240bfaf88c`; the d79 LES diagnostic checkpoint is `20ae6770924df15f31305aad56e44a54a3dd4d88`. Their manifests and existing result files are not rewritten. Use those commits to reproduce or interpret old d79 evidence. The current AMR-channel experiment requires its own GPU and applicable flow evidence.
|
||||
|
||||
## Scope and locked definitions
|
||||
|
||||
Explicitly pinned AMR-channel experimental production snapshot (see provenance below). Uniform D2Q9/MRT, shifted FP32, double buffer, no LES, stationary cylinder and no-slip walls. This is not an AMR, jet, or full canonical-outlet certification.
|
||||
|
||||
Physical geometry: [0,2.2] × [0,0.41], center (0.2,0.2), radius .05, rho=1, nu=.001, inlet mean=1 and peak=1.5. D is lattice diameter; dx=.1/D, dt=U_lattice*dx, nu_lattice=U_lattice*D/100. U_lattice=.01 gives peak inlet Mach .02598. Existing local Zou–He inlet is selected in validation config only. Existing pressure-NEQ outlet is unclamped: **it does not impose the canonical vector do-nothing condition nu*d_n(u)-p*n=0**. The existing production channel-flow initial condition is used, not zero velocity. Periodic eligibility is delayed to t*=250 (25 physical seconds), with fixed end t*=300 (30 seconds).
|
||||
|
||||
Official source: https://wwwold.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark2_re100.html
|
||||
|
||||
The 20141124 official newer reference archive has higher-order line-integration force data and supersedes the older example table for this comparison. The compact tests/validation/data/dfg_re100/reference_cycle.json preserves the last complete lift-minimum cycle from lv6/dt4. Reference Cd=[3.164266,3.227392], Cl=[-1.021286,.986571], pressure difference=[2.420371,2.507013], St=.301841. These are derived sampled extrema, not exact analytical values. Official mean=(min+max)/2 and amplitude=max-min; separate time averages are also recorded.
|
||||
|
||||
## Exact commands and startup ownership
|
||||
|
||||
Use the existing CUDA/PyCUDA environment; no package installation or production edits required. From the repository root, use a new task-owned output path:
|
||||
|
||||
export PYTHONPATH="$PWD/src${PYTHONPATH:+:$PYTHONPATH}"
|
||||
python -m pytest -q tests/unit/test_re100.py
|
||||
CUDA_VISIBLE_DEVICES=0 python tests/validation/run_dfg_re100.py --diameter 20 --speed .01 --mode pilot --output output/validation/pilot_d20_u001 > pilot.log 2>&1
|
||||
|
||||
Pilot is t*=5 (10,000 steps), with dense .02-t* sampling to calibrate diagnostic cost. It cannot pass periodicity or external benchmark validation. Root launch owner reviews health, memory, timing and stdout before full runs; the full pair is not launched automatically.
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0 python tests/validation/run_dfg_re100.py --diameter 40 --speed .01 --mode full --output output/validation/full_d40_u001 > d40.log 2>&1
|
||||
CUDA_VISIBLE_DEVICES=1 python tests/validation/run_dfg_re100.py --diameter 80 --speed .01 --mode full --output output/validation/full_d80_u001 > d80.log 2>&1
|
||||
|
||||
These are separate invocations, not a parallel shell script. A single root launch owner serializes startup/compilation across all Horsea work. Do not launch the next process until the previous log contains an explicit `READY {...}` line. READY comes after the first physical sample, including Simulation initialize, sim.run, read_force and get_macroscopic, so lazy runtime/diagnostic paths are exercised. That sample remains in the physical trajectory/history. No hidden warmup/reset, no later new diagnostic kernel. Do not treat a process merely existing as READY. Keep separate output directories and retain exit codes/logs. GPU assignment is chosen by root after checking existing workloads; shown indices are examples of physical device selection.
|
||||
|
||||
## Measurement, frozen criteria and outcomes
|
||||
|
||||
Force uses advancing link-momentum exchange, accumulated in batches <=100 steps, combined in FP64; Cd/Cl=2F/(U_lattice² D). Force timestamp is the batch midpoint. Pressure uses fluid-only h/2h extrapolation to cylinder front/back points and physical conversion 1/(3 U_lattice²), at each sample endpoint. For phase comparisons endpoint pressure is interpolated to the same cycle phase. Both raw times are saved.
|
||||
|
||||
Before t*=240 sample width=.5; afterwards .02 (about 166 samples per lift cycle). Pilot uses dense sampling throughout. Extract complete lift-minimum-to-minimum cycles using quadratic local minimum locations, accepting periods 2–5 t*. Latest five complete cycles must be consecutive with the last ending no more than 5 t* before the final observation, and must start at or after t*=250. Eligibility requires lift amplitude >=.2, period range/mean <=.005, Cd/Cl/pressure min and max ranges <=.005 absolute, cycle-mean mass range/mean <=1e-4, and each cycle's mean mass-flux imbalance <=.005. Instantaneous flux equality is not required in this unsteady compressible LBM diagnostic.
|
||||
|
||||
Reference gates: Cd extrema relative errors <=2%, Cl extrema absolute errors <=.03, pressure extrema relative errors <=2%, St relative error <=2%. These are predeclared engineering tolerances, not official uncertainty intervals. Phase-aligned waveform RMS errors, extrema, midpoint means, time means, amplitudes, outlet residual, sampled density bounds and Mach are all reported. Waveform RMS is diagnostic (no retroactive gate). No threshold changes after GPU results. At cap, failed periodicity stays incomplete. Invalid/nonfinite state, rho outside [.5,1.5], or sampled max Mach>.2 causes a safety stop; this is a gross-health guard, not a Mach-independence claim.
|
||||
|
||||
A case_reference_pass requires both periodicity and reference gates. Every raw case keeps suite_validated=false. The two-grid comparison must report each case and errors; matched-Mach D40/D80 is evidence of resolution sensitivity only, not an asymptotic convergence order. If either fails or finer errors increase materially, report the failure instead of adjusting thresholds. No full canonical/AMR/Mach-independent validation claim is allowed.
|
||||
|
||||
## Resource and calibration plan
|
||||
|
||||
D20: 441×84=37,044 nodes, pilot 10,000 steps. D40: 881×166=146,246 nodes, 1,200,000 full steps. D80: 1761×330=581,130 nodes, 2,400,000 full steps. Main double-buffer DDF arrays alone cost 72 bytes/node: 2.7/10.5/41.8 MB decimal. Flags, masks, macro host arrays, body buffers and CUDA context/JIT memory add overhead; root verifies actual free GPU/host memory and disk before launch. Conservative per-process reservation: 1 GiB VRAM and 2 GiB host RAM, excluding shared/compiler overhead; measurements take precedence.
|
||||
|
||||
Historical Re20 D80 U=.01 step throughput was ~6082 MLUPS (45.9 seconds/480k steps). At equal throughput D80 Re100 full advancing steps would take ~229 seconds, excluding dense full-field downloads/reduction and compile overhead. D40 estimate must be calibrated, not assumed proportional. Pilot records startup_seconds, step_seconds, measurement_seconds, mlups. For each full grid, calibrate a same-grid pilot if D20 cannot predict cost adequately. Full has about 3480 records (480 coarse+3000 dense); use measured seconds/step × full steps plus seconds/record ×3480 plus startup. D20-to-D80 extrapolation is only a rough planning upper/lower guide, not an ETA commitment.
|
||||
|
||||
Only geometry.npz, one final_field.npz, history.csv, summary.json and stdout/stderr are persisted. No per-step or repeated full-field snapshots. D80 final float64 rho/ux/uy plus mask are <15 MB uncompressed; history/summary several MB, compact versioned reference/source guards <50 KiB. Budget <100 MB per case and <500 MB whole campaign including logs and optional externally retained archives, safely below 2 GiB; root still checks baseline Horsea usage. Retain sparse evidence only.
|
||||
|
||||
## CPU validation and remaining execution
|
||||
|
||||
Ten tests cover geometry/units/config, pressure scaling, periodic and nonperiodic synthetic histories, immutable production hashes, and self-reference comparison. CUDA execution is not available in the preparation environment and has not been claimed. Run pilot first on Horsea; do not confuse successful CPU tests with GPU readiness.
|
||||
|
||||
## Reference provenance and reproduction
|
||||
|
||||
Only the compact derived cycle is versioned. Its source URL, official ZIP SHA-256, column selection, and cycle-selection description are embedded in reference_cycle.json. Raw archives and GPU results are not repository inputs. To reproduce it, obtain the exact archive identified there, verify its SHA-256, and extract logs/results_cc2d_bench2_referencenew/bdforces_q2_lv6_dt4 and pointvalues_q2_lv6_dt4. Use physical time column 2 multiplied by 10, drag/lift columns 4/5, and pressure columns 7 minus 12 (one-based columns). Build rows with mass=flux_in=flux_out=1, then call periodic.extract(rows, min_time=0) and select the last complete cycle. The source guard is tests/validation/data/dfg_re100/source_manifest.json; it verifies the current reviewed production snapshot rather than equating its origin with the validation branch HEAD.
|
||||
@@ -1,49 +0,0 @@
|
||||
# DFG 2D-1 (Re 20) against the official reference intervals
|
||||
|
||||
`tests/validation/run_dfg_re20.py` runs the steady DFG 2D-1 case on the refined
|
||||
channel root (D40 root, D80 wide fine patch) and on the uniform D80 grid. Geometry,
|
||||
inflow, initial state, layout and observation are those of the pinned Re100 runner
|
||||
(`run_refined_dfg.py`, `re100_helpers.py`, both unchanged); only the lattice
|
||||
viscosity is set for Re 20 (root tau 0.56, fine 0.62, uniform 0.62, lattice speed 0.01).
|
||||
|
||||
Label: "compared against the official reference bands", not a full benchmark
|
||||
certification: the outlet is pressure-NEQ (`neq_extrap`), not do-nothing, and the
|
||||
inlet is local Zou-He.
|
||||
|
||||
## Quantities
|
||||
|
||||
- Cd = 2 Fx / (rho U^2 D), Cl = 2 Fy / (rho U^2 D), interval means over each sample.
|
||||
- p_diff = p(0.15, 0.2) - p(0.25, 0.2), linear extrapolation to the exact surface
|
||||
points (`re100_helpers.pressure_probes`). The helper returns dp / (rho U^2) in
|
||||
scaled units; p_diff = dp * 0.04 for U = 0.2 m/s, rho = 1. Both are reported.
|
||||
- Official intervals (Schaefer & Turek 1996): Cd [5.57, 5.59], Cl [0.0104, 0.0110],
|
||||
p_diff [0.1172, 0.1176].
|
||||
|
||||
## Bounds and steady rule (frozen before any run)
|
||||
|
||||
- End time t* = 60 (30 s physical): 240,000 refined root steps, 480,000 uniform steps.
|
||||
- One sample every t* = 0.5 (0.25 s) at the end of a synchronized root step; Cd and
|
||||
Cl are interval means. Trailing window t* 50..60 inclusive (21 samples).
|
||||
- Steady rule (our engineering choice, not official): in the window, for each of Cd,
|
||||
Cl, p_diff, max - min <= 10 % and |least-squares slope x window| <= 2 % of its
|
||||
official interval width (range 2e-3 / 6e-5 / 4e-5; slope 4e-4 / 1.2e-5 / 8e-6).
|
||||
Absolute limits, so the small Cl is never a divisor. Never relaxed after a run.
|
||||
- The verdict uses the window mean; the distance to the nearest interval edge is
|
||||
reported for each quantity (negative outside).
|
||||
- Force sign: fluid on cylinder, x along the inflow, y up from the bottom wall; Cd > 0
|
||||
and the official Cl > 0 (`cd_positive` is reported).
|
||||
- Not steady at t* = 60: status `NOT_STEADY`, no interval verdict. Health limits
|
||||
(Mach <= 0.2, rho in [0.5, 1.5]) stop the run with `UNHEALTHY`.
|
||||
- Steady: `WITHIN_INTERVALS` or `OUTSIDE_INTERVALS`, with the per-quantity verdict.
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
python tests/validation/run_dfg_re20.py --output <new-dir> --plan
|
||||
CUDA_VISIBLE_DEVICES=<n> python tests/validation/run_dfg_re20.py --case both \
|
||||
--collision SRT --output <new-dir> --max-wall-seconds <cap>
|
||||
```
|
||||
|
||||
The uniform D80 control separates method error from refinement error. `--plan` writes `plan.json` only. A GPU run writes `plan.json` and `result.json`
|
||||
(per case: lattice parameters, every sample, window ranges, status, verdict).
|
||||
Host contract tests: `tests/unit/test_dfg_re20.py`.
|
||||
@@ -1,242 +0,0 @@
|
||||
# Prescribed in-line oscillation: Dütsch Re 100 / KC 5 — PROTOCOL (review draft)
|
||||
|
||||
Production under test: `claude/moving-bodies-s3a` at `653fd997` (unchanged).
|
||||
Primary source: Dütsch, Durst, Becker & Lienhart, J. Fluid Mech. 360, 249–271
|
||||
(1998), doi:10.1017/S002211209800860X. Source facts below were verified from
|
||||
the paper (Slack C0C78UFKGRM/1793335557.460509, TRON 1793356317–1793356724,
|
||||
page numbers given). Everything marked ENGINEERING is our a-priori choice,
|
||||
fixed before any benchmark result; it is not a paper uncertainty.
|
||||
|
||||
## 0. Source facts (with provenance)
|
||||
|
||||
- Motion (p.251): `x_c = -A sin(2 pi f t)`; paper phase phi = 0 is the mean
|
||||
position moving left at `-U_max`. KC = 5 gives `A/D = 5/(2 pi) = .7957747`.
|
||||
- Start (p.254): the computation starts a quarter cycle before phi = 0, at
|
||||
`x = +A` with zero velocity (phi = -90°).
|
||||
- Experiment (pp.252, 254–256): D = 10 mm, L = .65 m, tank 1.2 × .7 × .7 m,
|
||||
rho = 998.2 kg/m³, mu = 1.00818 mPa·s, Re = 100, KC = 5; LDA with 1° phase
|
||||
bins and 200 samples per node and degree; no quantified experimental error
|
||||
recovered.
|
||||
- Profiles (Fig. 8, p.260): LAB-FIXED stations x1 = -6, 0, 6, 12 mm, i.e.
|
||||
x/D = -.6, 0, .6, 1.2, at phi = 180°, 210°, 330°; u1 and u2 versus x2.
|
||||
- Original numerics (§2): cylinder-fixed accelerated frame (`x_i = x~_i +
|
||||
x_i,s`, outer velocity `-u_c`, inertial source `-rho du_c/dt`), O-grid outer
|
||||
diameter 120 D, illustrated 192 circumferential nodes, 720 steps/cycle; tank
|
||||
influence neglected.
|
||||
- Force fit (verified): force ON THE BODY per unit span
|
||||
`F1 = -0.5 rho D c_d xdot|xdot| - (pi/4) rho D² c_i xddot` with `x = -A sin phi`;
|
||||
normalized by `0.5 rho D U_max²`: `c_d cos phi |cos phi| - (pi² c_i / KC) sin phi`.
|
||||
`c_i` is the ADDED-MASS coefficient; total inertia `c_m = c_i + 1` is a
|
||||
different convention and is not used.
|
||||
- Table 1 computed Morison coefficients (c_d, c_i) for 96 / 192 / 384 perimeter
|
||||
nodes at dt/T = 1/720: (2.15, 1.42), (2.10, 1.45), (2.09, 1.45). These are
|
||||
fitted computed coefficients, not a measured force and not an error bar. The
|
||||
paper states the coarse direct force is closer to the finest direct force
|
||||
than the Morison fit, but gives no waveform-error norm.
|
||||
|
||||
## 1. Kinematics, time levels and phase bookkeeping
|
||||
|
||||
- Our run is in the LAB frame: the circle moves, the fluid is initially at rest
|
||||
(rho = 1, u = 0); no fictitious inertial source is added. Lab-fixed profile
|
||||
stations are compared directly (no frame transform needed).
|
||||
- Geometry reference: the circle's base centre is the INITIAL position, the
|
||||
right turning point `x_mean + A`; the declared offset box is x in [-2A, 0],
|
||||
y = 0. The body is never teleported: offset 0 at s = 0.
|
||||
- Elapsed time s = n dt from the start. Analytic position relative to the mean
|
||||
`x(s) = A cos(2 pi s / T)`, velocity `u(s) = -U_max sin(2 pi s / T)`; paper
|
||||
phase `phi = (360 s / T - 90) mod 360`.
|
||||
- Step n moves from s_n to s_{n+1}: target offset `x(s_{n+1}) - A`; wall
|
||||
velocity = the single-step interval average `(x(s_{n+1}) - x(s_n)) / dt`
|
||||
(analytic, supplied, not differenced from stored offsets). The per-step
|
||||
force of `run(1)` / `read_force(normalize=True)` is attributed to that same
|
||||
interval.
|
||||
- Paper cycle k = 1, 2, ... spans `s in [T/4 + (k-1) T, T/4 + k T)`; the
|
||||
initial quarter cycle (s < T/4) is cycle 0 and never scored. Every logged
|
||||
sample carries (k, phi). Example: phi = 330° of cycle 1 is at s = 7T/6.
|
||||
- T is chosen as a multiple of 360 native steps, so phi = 180°, 210°, 330° fall
|
||||
exactly on step boundaries and 1° bins hold whole steps.
|
||||
|
||||
## 2. Resolution ladder (uniform grid, fixed Mach), ENGINEERING
|
||||
|
||||
| D (cells) | T (steps) | U_max = 5 D / T | nu_lat = U_max D / 100 | tau (3 nu + .5) |
|
||||
|---|---|---|---|---|
|
||||
| 20 | 2,520 | .039683 | .0079365 | .5238 |
|
||||
| 40 | 5,040 | .039683 | .015873 | .5476 |
|
||||
| 80 | 10,080 | .039683 | .031746 | .5952 |
|
||||
|
||||
- MRT collision. Mach fixed (Ma ≈ .0687); a separate Mach check at D = 40
|
||||
with T = 10,080 (U_max halved, tau .5238) is labelled MACH and not part of the
|
||||
ladder.
|
||||
- Per-step displacement ≤ U_max ≈ .04 cells (bound .5).
|
||||
|
||||
## 3. Domain and boundaries (finite-domain reproduction), ENGINEERING
|
||||
|
||||
The paper's 120 D O-grid is not reproduced; our rectangular box is a
|
||||
reproduction choice. The native dense channel (zero-velocity inlet,
|
||||
pressure-NEQ outlet, bounce-back y walls) is a finite-domain approximation,
|
||||
not infinite still fluid. The G4 x-periodic / y-slip override is a separately
|
||||
labelled sensitivity family and never mixed into the primary ladder. The
|
||||
runner owner confirms the zero-velocity inlet is supported as configured
|
||||
before any run.
|
||||
|
||||
Order (closed):
|
||||
1. Smoke + ledger at D = 40, native BC, 40 D × 40 D (finite box).
|
||||
2. Domain sensitivity before any literature statement: same BC, D = 40,
|
||||
L ∈ {20 D, 40 D, 60 D}; record the change in Fx waveform, c_d, c_i and the
|
||||
scored profiles.
|
||||
3. Optional labelled BC sensitivity: one x-periodic / y-slip case at D = 40,
|
||||
40 D; reported as a delta against native only.
|
||||
4. Only after step 2 passes: the D = 20 / 40 / 80 ladder on the chosen BC and
|
||||
domain.
|
||||
|
||||
Domain criteria (a-priori, ENGINEERING; separate from the Table 1 bands): the
|
||||
reference is 60 D at D = 40; 20 D and 40 D are compared with it on the common
|
||||
window (same cycle indices) and the common valid point set:
|
||||
- `RMS(Fx_L - Fx_60D) / F_ref ≤ .01` over the window (per-step waveforms);
|
||||
- `|c_d,L - c_d,60D| ≤ .02` and `|c_i,L - c_i,60D| ≤ .02` (window fits);
|
||||
- `max(|u_L - u_60D|, |v_L - v_60D|)` over common points `/ U_max ≤ .02`.
|
||||
The chosen domain is the smallest of {20 D, 40 D} passing all three; if
|
||||
neither passes, the domain verdict is FAIL and the ladder is not run.
|
||||
- Memory/runtime: measured by a smoke run before each new size; measurements
|
||||
inform scheduling only, never tolerances.
|
||||
|
||||
## 4. Observables (raw, per step, preserved)
|
||||
|
||||
- `obs` Fx, Fy (CME) and all `MovingTables.DIAG` columns per step: CME,
|
||||
CME + Wen and CME + cover − uncover reported separately; crossings, eq_only,
|
||||
no_donor (no_donor must stay 0).
|
||||
- Velocity u1, u2 on lab-fixed lines x/D = -.6, 0, .6, 1.2 (relative to the
|
||||
mean cylinder position) versus x2, at the scored phases. Lab frame
|
||||
throughout; if any body-frame intermediate is used, position and velocity
|
||||
are both transformed.
|
||||
- Sampling cost: raw force and diagnostics counters are read every step
|
||||
(small). Flags and velocity fields are read only at the steps inside the
|
||||
three prescribed 1° phase bins (φ ∈ [180, 181), [210, 211), [330, 331),
|
||||
T/360 steps each) of the cycles needed for settling and scoring, plus
|
||||
sparse declared audits; never every step.
|
||||
- Point set: fixed x2/D sample points per station, identical on every grid
|
||||
and declared before running; per point the interpolation weights are
|
||||
recorded; the scored set is the common subset valid on all compared grids
|
||||
at that phase, with its coverage recorded; point sets never change silently.
|
||||
- Interpolation: bilinear on cell centres using only cells that are fluid at
|
||||
that step (flags read back via `get_flags()`); weights renormalized over the
|
||||
fluid corners; a sample with no fluid corner, or inside the circle at that
|
||||
step, is MISSING and logged, never set to zero. One-degree bins keep valid
|
||||
fluid weights. The numerical average need not mimic the 200 LDA samples.
|
||||
|
||||
## 5. Periodic-regime gate (every case, before any statistic), ENGINEERING
|
||||
|
||||
Ledger rows, same schema for every case: dx, dt (in D and T units); U_max, nu,
|
||||
Re, KC (lattice and nondimensional); T (native steps, nondimensional);
|
||||
duration (native steps, s/T, paper cycles); discard; accepted window; cycle
|
||||
ledger; verdict PASS or FAIL-CLOSED.
|
||||
|
||||
Scales: `F_ref = 0.5 rho U_max² D`, `U_ref = U_max`; never a near-zero mean
|
||||
force as denominator.
|
||||
|
||||
- Settling metrics, cycle k−1 → k, phase-aligned by step within the cycle
|
||||
(error budget: one fifth of the tightest comparison band of the same
|
||||
observable, so residual drift cannot obscure a domain or resolution pass):
|
||||
- waveform `RMS(dFx) / F_ref ≤ .002`;
|
||||
- `|d mean|`, `|d RMS|`, `|d peak|` of Fx, each `/ F_ref ≤ .002`;
|
||||
- scored profiles (fluid-only) `|du| / U_max`, `|dv| / U_max ≤ .004` at the
|
||||
three phases × four stations;
|
||||
- Morison `|dc_d| ≤ .004`, `|dc_i| ≤ .004`.
|
||||
- Conservative comparison: every domain and cross-resolution criterion is
|
||||
applied to `|difference| + drift_a + drift_b`, where drift is the largest
|
||||
pair metric of that case inside the scored window.
|
||||
- These constants (protocol.py SETTLE, DOMAIN, CROSS) are frozen before any
|
||||
physical result; no post-result loosening.
|
||||
- Rule: all metrics for N = 3 consecutive whole paper cycles → settled.
|
||||
|
||||
Cycle indexing (worked example, binding). Paper cycle k spans
|
||||
`s/T in [0.25 + (k-1), 0.25 + k)`, i.e. φ0 → φ0; cycle 0 is the startup
|
||||
quarter `s/T < 0.25`. Pair p_k compares cycle k−1 with cycle k.
|
||||
|
||||
- Discarded: cycles 0, 1, 2 (s/T < 2.25). Pairs are formed only between
|
||||
cycles ≥ 3, so the first pair is p_4 (3 → 4).
|
||||
- Settling proof for a case: three consecutive pairs all pass; K_case = the
|
||||
first cycle AFTER the last of those pairs. Earliest possible: pairs 3→4,
|
||||
4→5, 5→6 pass ⇒ K_case = 7.
|
||||
- Common window: K = max over compared cases of K_case; every case scores the
|
||||
same three complete cycles K, K+1, K+2 and must stay settled continuously:
|
||||
every pair p_j for j = its own K_case .. K+2 must pass (including the wait
|
||||
between its local settle and the common K), otherwise that case is
|
||||
FAIL-CLOSED.
|
||||
- Cap: cycle k ends at s/T = k + 0.25, so the last admissible scored cycle is
|
||||
11 (ends 11.25 T) and K ≤ 9. Examples: K = 7 → cycles 7–9 (ends 9.25 T);
|
||||
K = 9 → cycles 9–11 (ends 11.25 T); any K_case > 9 → FAIL-CLOSED.
|
||||
- Run length: cases run sequentially; each runs to the end of cycle 11
|
||||
(11.25 T: 28,350 / 56,700 / 113,400 steps for D = 20 / 40 / 80; the 12 T
|
||||
cap is 30,240 / 60,480 / 120,960) and keeps its scalar and profile data, then
|
||||
the common K is chosen. An unscorable partial cycle 12 is not run.
|
||||
- Profiles: an empty common valid point set is FAIL-CLOSED; coverage, missing
|
||||
points, bin time weights (equal per step) and the original fluid corner
|
||||
weight sums are reported.
|
||||
- The earlier G4 timing check (100 coarse vs 400 diffusive-fine steps) was a
|
||||
matched short transient, not periodic evidence.
|
||||
|
||||
## 6. Metrics and per-observable criteria (ENGINEERING, not paper uncertainty)
|
||||
|
||||
Three criteria stay separate; the G4 same-grid 2% target is not reused.
|
||||
|
||||
- Morison fit: least squares of the normalized per-step force over the
|
||||
accepted window against the two basis functions, each averaged over its
|
||||
single step interval, fixed paper phase, no free phase shift; c_d and
|
||||
added-mass c_i per force candidate.
|
||||
- Cross-resolution: every scored observable's error against its reference must
|
||||
decrease monotonically D20 → D40 → D80 (necessary, not sufficient).
|
||||
- Against Table 1 (computed fit, not measured force): at D ≥ 40,
|
||||
`|c_d - 2.09| ≤ .15` and `|c_i - 1.45| ≤ .15`, and monotone.
|
||||
- Force waveform against its own Morison reconstruction: residual reported
|
||||
only, no pass/fail (the paper reports fit error above its coarse numerical
|
||||
error and gives no percentage).
|
||||
- Fig. 8 LDA profiles: QUALITATIVE ONLY. The original figure could not be
|
||||
accessed or rendered; no digitized ordinates and no measurement uncertainty
|
||||
exist. After a digitization with figure number, tool and uncertainty note:
|
||||
`RMS(|du|, |dv|) / U_max ≤ .15` at D ≥ 40 and monotone; LDA error stays
|
||||
UNQUANTIFIED and is stated with every comparison.
|
||||
- Fy sanity (not literature): `|mean Fy| / F_ref ≤ .05` in the accepted window.
|
||||
- No force candidate is selected for an RHS by these results; each is
|
||||
reported.
|
||||
|
||||
Superseded (earlier drafts): the velocity-jump start, guessed stations and
|
||||
phases, "C_M" for c_i, and the uncertainty wording; cycles 4–6, the 30-cycle
|
||||
cap and the earlier tolerance numbers.
|
||||
|
||||
## 6b. Amendment: duration-only diagnostic (declared before the run)
|
||||
|
||||
- Background: the D40 domain stage (20 D / 40 D / 60 D, 11.25 T each) failed
|
||||
settling in every box (no window). That result stands.
|
||||
- One declared diagnostic: D = 40, 60 D box, from rest, through cycle 31
|
||||
(157,500 steps = 31.25 T). Physics, settling tolerances, bands and
|
||||
production unchanged; three proof pairs plus a continuously settled
|
||||
K..K+2 with K ≤ 29.
|
||||
- Invocation: `run.py --D 40 --box 60 --cycles 31` then
|
||||
`analyze.py --horizon 31`; the JSON records `horizon` and `horizon_mode`.
|
||||
Without `--horizon` the analyzer is the original cap-11 scoring.
|
||||
- Failure at 31.25 T: report and stop; no automatic extension, matrix, D80
|
||||
or VIV. A pass shows only that this box settles with time; it is not a
|
||||
domain or literature pass.
|
||||
- The phase-locked high-frequency force component (residual of the mean
|
||||
waveform after its Morison reconstruction, above 20/T) is reported per case
|
||||
as unqualified; it needs separate qualification before any Morison or
|
||||
literature statement.
|
||||
|
||||
## 6a. Driver unit tests (CPU, before any run)
|
||||
|
||||
- Phase/cycle arithmetic: s ↔ (k, φ), cycle boundaries, bin membership,
|
||||
window/cap selection for K = 7, 8, 9, 10.
|
||||
- Solid-aware interpolation: all-fluid, partial-solid (renormalized weights),
|
||||
no-fluid (MISSING) and inside-circle cases.
|
||||
- Interval-averaged Morison bases and the two-parameter fit on a synthetic
|
||||
waveform with known c_d, c_i.
|
||||
|
||||
## 7. Order and scope
|
||||
|
||||
Protocol review → one-shot Dütsch driver (analytic `set_body` + `run(1)`,
|
||||
cycle ledger, lab-fixed fluid-only station sampler, two-basis Morison fit; no
|
||||
reusable runner until the first smoke shows shared helpers pay off; no
|
||||
production change) → smoke (memory/runtime) → domain sensitivity →
|
||||
ladder → physical-validation verdict → only then single-cylinder VIV. No AMR,
|
||||
no pinball, no manuscript work.
|
||||
@@ -1,115 +0,0 @@
|
||||
# Jet-circle closed-control-volume pilot
|
||||
|
||||
## Current experimental source provenance
|
||||
|
||||
This development branch now includes the reviewed AMR-channel source changes originating in `5338864748ac67fe2544487c059ba149ebf916da` and `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. The current full-source manifest pins the latter reviewed source snapshot, including the new channel files. This is an explicit new experiment; neither source review nor host tests transfer historical d79 GPU/flow acceptance to it. The channel GPU gate has not yet run at integration time.
|
||||
|
||||
New run JSON records the source-origin pin plus `checkout_revision` (actual Git HEAD), `checkout_dirty`, and the experimental-source label. The origin pin is not claimed to be the run's checkout commit. Numerical definitions and thresholds are unchanged. Runs require a Git checkout for this runtime identity; source hash checks remain active.
|
||||
|
||||
Historical d79 Re100/Jet execution stays reproducible at `69e9b78de9483a5ff21e2d0c74311d240bfaf88c`; the d79 LES diagnostic checkpoint is `20ae6770924df15f31305aad56e44a54a3dd4d88`. Their manifests and existing result files are not rewritten. Use those commits to reproduce or interpret old d79 evidence. The current AMR-channel experiment requires its own GPU and applicable flow evidence.
|
||||
|
||||
Status: CPU preparation only. No GPU run or production change. Current source origin: reviewed AMR-channel snapshot 47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5; this new experiment has no inherited d79 GPU acceptance. tests/validation/data/jet_circle_cv/source_manifest.json checks all tracked source file hashes before importing GPU code.
|
||||
|
||||
## Execute only after root-owned GPU serialization
|
||||
|
||||
From the repository root, with REPO set to the repository root and an absent output directory:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m pytest -q tests/unit/test_jet_circle_cv.py
|
||||
python tests/validation/run_jet_circle_cv.py --repo "$REPO" --output "$OUTPUT" --plan
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$REPO" --output "$OUTPUT"
|
||||
```
|
||||
|
||||
The runner does not select, reserve, or contact a GPU machine. It requires one explicit CUDA_VISIBLE_DEVICES value, at least 1 GiB free, and refuses an existing output directory. It imports existing PyCUDA; no installation. Default pilot: 800 base steps, 8 output windows, at most 1800 seconds after READY. READY is printed only after both steppers compile, execute one base/two fine warmup hops, exercise download/raw paths, and reset initial state/time. A pre-READY silence may be compilation, not stepping progress. Root owns any overall startup timeout and all launch serialization.
|
||||
|
||||
## Physical/numerical case
|
||||
|
||||
Periodic root box 192 by 128, center (96,64) in root cell-edge coordinates, D=24.084, U0=.03, nu=.0072252, Re_D=100, beta=Vj/U0=.1, rho0=1. Unforced D2Q9 SRT FP32, no DDF shifting, no LES. The slightly noninteger diameter was selected solely to avoid q clipping at all three planned scales. Circle is stationary. Two uniform radial sectors at pi/2 and 3pi/2, half-width pi/12: top blows outward +.003, bottom sucks inward -.003; remaining wall has zero velocity. This periodic transient is NOT DFG, infinite-stream drag, or statistical high-Re validation. End time is t*=t U0/D approximately .9965.
|
||||
|
||||
AMR bounds (48,32,144,96). Finest dx=dt=.5 in root lattice units, nu_fine=2 nu, finest D=48.168. Full-domain fine reference is 384 by 256 with exactly the same finest spacing, timestep, viscosity, collision and storage. Root tau=.5216756, fine tau=.5433512, Mach approximately .05196.
|
||||
|
||||
AMR uses actual public FixedPatchStepper Jet construction and raw observation. The uniform blocked reference reuses the existing test_raw_exchange_gpu.py optional-input validation adapter: production Jet-derived velocity/force-arm arrays feed the unchanged blocked curved kernel. Its normal public PeriodicBlockStepper API is passive-only. This does not claim there is no uniform Jet route anywhere: Simulation.add_jet exists, but the same-blocked-kernel adapter removes a backend/storage confound for this comparison. No new public backend is introduced. Runtime asserts matching directions, q, fallback and true hit coordinates after frame conversion; both sectors must resolve links. Unclamped q/no fallback is required.
|
||||
|
||||
## Independent measurements and signs
|
||||
|
||||
Two closed rectangles: (72,44,120,84) and (64,40,128,88), wholly within the finest patch. Every contour face has two active-fluid stencil layers. Capture physical postcollision populations at t=0 and every completed base interval. Collision conserves density/momentum; these are valid endpoint moments, NOT precollision stress estimates.
|
||||
|
||||
For each contour, M and P sum fluid-cell rho and rho*u times h^2, excluding the staircase solid. Face midpoint quadrature integrates rho*u dot n and [rho*u*u+p*I-mu*(grad u+grad u.T)] dot n. Velocity gradients use centered differences in root-length units, mu=rho*nu, p=(rho-1)/3. Constant pressure subtraction integrates to zero on a closed rectangle. No raw exchange enters these field quantities.
|
||||
|
||||
Trapezoidal time integration samples every base step over the SAME reported [step_start,step_end] as cumulative raw impulses/masses. For each window:
|
||||
|
||||
- mass into fluid through body surface = delta M + integral outer mass flux
|
||||
- raw boundary reaction target = -delta P - integral outer momentum-minus-traction flux
|
||||
|
||||
Raw rows are [Jx,Jy,angular impulse,mass into fluid]; each uniform fine hop scales by [.25,.25,.125,.25], exactly as the AMR interval ledger. Both fine hops are accumulated. AMR observation time endpoints and the public raw reduction are checked. Reports separate total, wall, aperture, blowing and suction raw rows. Coefficients divide force by .5*rho0*U0^2*D. These are momentum reactions including aperture transport; neither raw aperture nor total raw reaction is labelled solid-only hydrodynamic traction.
|
||||
|
||||
Independent aperture mass estimates integrate measured rho*u dot e_r over true arcs using 64-point Gauss quadrature and nearest active-fluid samples. No target velocity or raw row enters this estimate. Reports include maximum sampling distance, actual signed raw mass rates and nominal signed volume flux ±2*(pi/12)*R*Vj. Near-wall nearest-node sampling is only a first-order diagnostic, not certified wall extrapolation. Nominal volume flux is not asserted to equal measured mass flow at finite density variation.
|
||||
|
||||
Inventory near the circle is a staircase-cell approximation; its geometric error and face/time quadrature errors can appear in CV residuals. Two-contour spread and grid refinement are essential. No exact continuum force value, solid traction recovery, jet hardware load, or physics acceptance is claimed. Output status is deliberately PILOT_DIAGNOSTIC_ONLY even on successful execution.
|
||||
|
||||
## Scope, refinement, resources
|
||||
|
||||
Default one pilot only. Estimated device envelope 128 MiB, required free 1 GiB; bounded JSON only, conservatively under 2 MiB, no population snapshots or per-step logs. Every base step downloads both fields for independent quadrature, so PCIe/CPU diagnostic cost is intentional. Six CPU tests cover descriptor/source identity, uniform flux cancellation, affine convective divergence, quadratic viscous sign/coefficient, matched geometry/Jet selection and diffusive scaling.
|
||||
|
||||
Optional --scale 2 or 4 doubles/quadruples all lengths and divides U,V by that scale, holds lattice nu fixed, and scales steps/windows by scale^2: physical Re and t* match, Mach decreases. Source and geometry guards repeat. Device estimate scales as scale^2; memory gate requires twice the estimate or 1 GiB, whichever is greater. Scale 4 is not part of default authorization/readiness. --re 400 requires --allow-re400 and a separate reviewed root launch decision after Re100 evidence; no automatic promotion.
|
||||
|
||||
Compare AMR/reference velocity error, signed reaction coefficients, each contour's independently measured reaction and residual, contour spread, signed mass budgets, and measured aperture flux. Freeze numerical tolerances only after agreeing a scientifically appropriate convergence criterion; this preparation imposes only structural/finite-state guards. A completed pilot cannot establish convergence.
|
||||
|
||||
Follow-on design retained: porous Couette crossflow with u=U*expm1(Re_V*y/H)/expm1(Re_V), v=V and unequal exact wall shear. It requires validation-only planar geometry and is intentionally not implemented here.
|
||||
|
||||
### Amplitude, geometry, and high-Re limits
|
||||
|
||||
Both Jet amplitudes are constant throughout the run and every fine substep; geometry is fixed. There is no runtime switch, per-step forcing schedule, or moving-wall velocity in this campaign. Construction/packing happens once, with identical uploaded amplitudes in the uniform adapter. This avoids any substep timing ambiguity from amplitude changes.
|
||||
|
||||
Every case records actual tau_root and tau_fine. For this fixed s=1 geometry and speed, Re400 would give nu=.0018063, tau_root=.5054189 and tau_fine=.5108378, close to the SRT limit .5. The implemented Re choices are a bounded campaign menu, not a universal physical Reynolds-number ceiling. Higher-Re evidence may require a larger initial resolved geometry or a different justified grid/relaxation design before launch. Diffusive refinement at constant lattice nu improves spatial/Mach resolution but does not move tau farther from .5; simply selecting scale=2 does not cure a poor tau. No all-Re, LES or three-dimensional realism claim follows from this pilot.
|
||||
|
||||
The source guard checks production file content against the current reviewed source origin, not validation-branch HEAD. This runner is validation-only; generated results and frozen execution-package archives are not committed.
|
||||
|
||||
## Measurement-only continuation (frozen design before execution)
|
||||
|
||||
The original 800-step pilot and its audit remain historical evidence, not an acceptance target. The following adds diagnostics only; it neither modifies production kernels nor adjusts velocity commands to improve measured flux. Use the canonical validation Git branch, with the same source-manifest check and checkout provenance record. No standalone replacement source archive is needed.
|
||||
|
||||
Run the pair serially under root-owned GPU allocation, from the checked-out repository:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_jet_circle_cv.py -v
|
||||
python tests/unit/test_jet_cv_measurements.py -v
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_OUTPUT_S1" --continuation --steps 3200 --scale 1
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_OUTPUT_S2" --continuation --steps 3200 --scale 2
|
||||
python tests/validation/analyze_jet_cv_continuation.py "$NEW_OUTPUT_S1" "$NEW_OUTPUT_S2" --output "$COMPARISON_JSON"
|
||||
```
|
||||
|
||||
Both output directories must be absent. `--plan` prints configuration without CUDA. `--continuation` deliberately accepts only Re100, scale1/2, and 3200 reference steps: a bounded campaign, not a physical Re ceiling. Actual steps are 3200 and 12800, reaching the same t*=3.986048829. Physical circle/box/contours and beta remain fixed; all root lengths grow by s, U and jet speed shrink by s, lattice nu remains .0072252. Both runs retain tau_root=.5216756 and tau_fine=.5433512, with Mach .0519615/s. This diffusive refinement changes the finite-Mach/compressibility error as well as grid resolution; it is not an isolated spatial-error order study.
|
||||
|
||||
### Units, cadence, and reconstructibility
|
||||
|
||||
Reference length/time are one scale-1 root lattice length/time; density reference is one. Convert scale-s root quantities by: length /s, time /s², velocity times s, mass /s², momentum and impulse /s, force times s, angular impulse /s², pressure times s². Volume and mass rates are unchanged. The nominal signed aperture volume rate is ±.0189155293672 in reference units at every scale. A mass flux divided by rho_ref times that rate is a reference-density diagnostic, not an actual-volume-flow ratio. Saved Q and M remain separate.
|
||||
|
||||
Exact raw observations accumulate every completed base interval, including both fine substeps. Field downloads occur only at shared reference times 0..100 at dt=1, then 100..3200 at dt=4: 876 samples per run, compared with 3201/12801 possible every-base samples. Every 100-reference-time window boundary is included. Trapezoidal integration uses the actual variable time gap, with the raw ledger covering exactly the same endpoints. No missing sample is silently treated as unit dt. Sampling is denser during abrupt startup; it is not a guarantee of resolving all high-frequency acoustic modes. Offline coarsening can expose some time-quadrature sensitivity but cannot rule out unresolved-frequency aliasing.
|
||||
|
||||
`samples_s*.npz` stores at every sample: density minima/maxima/max perturbation and max speed for uniform fluid, AMR root leaves and AMR fine leaves; composite full-domain mass; CV endpoint inventory and flux; separate advective, pressure and negative-viscous-traction momentum fluxes; nearest-fluid mass proxy; extrapolation ray primitive values; linear/quadratic reconstructed Q and M; and cumulative raw wall/aperture/total/blowing/suction rows. Associated JSON identifies axes, units, conversion factors, hashes and snapshot metadata. Composite AMR mass sums root leaves at area1 and fine leaves at area.25, then divides by s². Covered cells are excluded. Global periodic mass change minus total raw body mass is reported, so a transfer/interface mass error cannot hide behind the two contours lying wholly within the fine patch.
|
||||
|
||||
Five compressed primitive snapshots at reference times 0,100,800,1600,3200 retain rho, reference velocity, origin and spacing for uniform, AMR root and AMR fine grids. Inactive/solid entries are NaN. These are full primitive grids at sparse times, not downsampled images or distributions. They permit independent spatial quadrature, field inspection, masks and inventory reconstruction. Per-sample split flux/inventory arrays allow independent temporal integration without rerunning GPU work. FP32 saved primitive snapshots introduce their own rounding; FP64 sampled diagnostic/ray arrays remain in the sample file.
|
||||
|
||||
### Independent aperture estimators and limitations
|
||||
|
||||
Thirty-two Gauss points per true aperture arc, with rays at distances 1.5,2.5,3.5 finest-cell widths into the fluid, sample rho,u,v by bilinear interpolation using only active fluid nodes. No commanded wall value or raw population enters reconstruction. Linear and quadratic outward-normal extrapolations separately reconstruct rho and u; integrate Q=integral(u.n ds) and M=integral(rho*u.n ds). Retain ray positions, normals, weights, primitive samples and coefficients, making every estimate reproducible. The nearest-fluid 64-point mass proxy is preserved for continuity with the pilot and remains explicitly a proxy.
|
||||
|
||||
Neither estimator is an exact wall value or validated wall model. Extrapolation amplifies noise and may yield unphysical density; do not clamp it to force agreement. Sector endpoints contain nonsmooth prescribed velocity changes, and interpolation may mix their neighborhood. Linear-versus-quadratic spread, ray density extrema, resolution changes and analytic manufactured tests constrain interpretation, but their agreement is not proof of low bias. Two grids alone cannot establish asymptotic convergence. Nominal geometry/profile flux is exact for the commanded uniform radial profile; comparison with reconstructed Q diagnoses estimator plus numerical-boundary error, not automatically a Jet defect. Raw reaction remains momentum transport plus traction; no nozzle-load or solid-only force claim.
|
||||
|
||||
### Predeclared comparison criteria
|
||||
|
||||
These criteria are fixed before the continuation runs. There is no new physical acceptance threshold and no tolerance fitted to the pilot:
|
||||
|
||||
1. Structural validity: exact source/provenance, matched geometry/parameters, unclamped q/no fallback, both apertures resolved, finite positive active-node density and finite velocity at every saved field sample, complete scheduled samples, contiguous matching raw intervals, and all declared files/hashes. Runtime does not promise positivity between saved samples.
|
||||
2. Report every window's signed and gross quantities, maximum sampled errors, and early (reference t<=800) versus later (t>800) norms separately. Normalize mass residual by rho_ref*abs(Q_command)*window_duration, and reaction residual by .5*rho_ref*U_ref²*D_ref*duration. Avoid dividing by a near-zero net mass or instantaneous force.
|
||||
3. Recompute contour residuals using the complete sampled grid and every second/fourth sample, always retaining endpoints. Save their changes as a temporal-sensitivity proxy, not an error bound. Compare both contours and both backends; show mass, Cx and Cy components separately.
|
||||
4. A directionally resolved reduction in a component's RMS CV residual is flagged only if coarse-minus-fine RMS exceeds twice the largest observed temporal coarsening change. This is an evidence-of-trend flag, never a physics PASS. If a component plateaus or grows, decompose temporal/spatial/staircase/boundary effects before proposing production changes.
|
||||
5. Compare reconstructed Q/command and M/(rho_ref*command) separately, at identical physical windows, retaining both extrapolations. Regard refinement improvement as persuasive only when it exceeds estimator spread and temporal variation. Inspect density and global mass alongside it. Agreement of AMR and uniform ledgers alone cannot validate their shared boundary treatment.
|
||||
6. Four D/U in a periodic unforced domain remains transient consistency evidence. Two scales do not prove order, stable shedding, infinite-stream drag, all-Re capability, or readiness to promote Re400. Formal physical acceptance needs an additional reference/error budget and appropriate time/domain evidence.
|
||||
|
||||
### Runtime and storage budget
|
||||
|
||||
Actual earlier scale-1 pilot cost was 52.3884s launch-to-exit, 46.1414s after READY for800 steps with full-field diagnostics every base step. A naive same-diagnostic3200-step scale-1 extrapolation is about210s; the new876 field samples reduce transfer count but add ray diagnostics and compression. Use conservative planning envelopes210–500s for scale1 and900–3200s for scale2, not timing guarantees. The scale2 conservative envelope accounts for4x cells and4x root steps; sparse sampling mitigates but does not eliminate that cost. Post-READY safety caps are1200s and5400s respectively, replacing the unsuitable fixed1800s assumption. Root owns compilation/startup timeout and serial launch decisions.
|
||||
|
||||
Device-envelope estimates remain128/512MiB for s1/s2, at least1GiB free (or twice estimate). Output estimate is under64MiB per run including five primitive snapshots and876 sample records; no all-time populations are saved. Plan/metadata and sampled files are rewritten at each100-reference-time window to retain bounded partial evidence if interrupted. READY still follows compiled warmup and state/time reset, before measured stepping begins.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Smagorinsky baseline/candidate helper diagnostic
|
||||
|
||||
This validation-only diagnostic compares exact d79 production headers with a temporary candidate. It does not modify production source. The candidate changes only the D2Q9 and D3Q19 closed-form factors from 18 to 18 sqrt(2), plus comments defining |S| = sqrt(2 S:S) and self-consistent effective tau. The small reviewed patch and expected header hashes are in `tests/validation/les/data`; no copied headers, solver trees, raw results, or binary fixtures are versioned.
|
||||
|
||||
## Prepare and verify on the CPU
|
||||
|
||||
From the repository root, choose a new output directory:
|
||||
|
||||
```sh
|
||||
python tests/validation/les/prepare.py --output-dir output/les-host/candidate
|
||||
python tests/validation/les/test_candidate.py --candidate-dir output/les-host/candidate --output output/les-host/host_results.json
|
||||
```
|
||||
|
||||
Preparation checks both actual production header hashes against d79, copies only `descriptors.cuh` and `turbulence_smag.cuh` into output-local baseline/patched include trees, applies the reviewed patch only to the latter, and verifies all four resulting hashes. It refuses an existing candidate directory. The host test verifies candidate hashes again and refuses an existing evidence file. The Git branch HEAD may include validation commits; header content, rather than HEAD identity, is the pin.
|
||||
|
||||
The independent host oracle constructs second-order Hermite populations and solves the independently derived quadratic by bisection:
|
||||
|
||||
tau * (tau - tau0) = 9 Cs^2 ||Pi_neq|| / (sqrt(2) rho)
|
||||
|
||||
There are **736 parameter cases**, covering D2Q9/D3Q19; Cs=0,.08,.16,.24; tau0=.6,.8,1.2; rho=.7,1,1.3; equilibrium, isotropic stress, pure shear, and trace-free diagonal extension; all coordinate planes and both signs; density-scaled Pi and separate fixed-Pi/nonunit-rho probes. The test checks quadratic residuals, closed/default-three/converged-200 iterations, bit-identical unchanged iterative output, and an additional explicit cap-active manufactured probe. Existing g++ and Python stdlib are sufficient; no installation is attempted. Compilation products are temporary. Previously reviewed host maxima were omega error 1.394302e-7 and quadratic residual 1.201202e-7; this is not CUDA fast-math evidence.
|
||||
|
||||
## Authorized GPU diagnostic
|
||||
|
||||
Use the existing working PyCUDA/NumPy environment and an explicitly selected device, through the same serialized GPU launch owner as the other campaigns:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/les/run_gpu.py --run-gpu --device 0 --output-dir output/les-gpu
|
||||
```
|
||||
|
||||
Device 0 is the logical device after CUDA visibility filtering. The output directory must not exist. The runner materializes and verifies the isolated include trees under `output/les-gpu/candidate`, then runs baseline and patched processes sequentially. Each process compiles one SourceModule at a time synchronously and synchronizes each probe. There are no background jobs, worker pools, or parallel compiler calls. No machine contact, device selection, reservation, or environment setup is performed by the runner.
|
||||
|
||||
Each version has exactly **48 scalar GPU cases**, 96 total: D2Q9/D3Q19, Cs=.16/0, equilibrium/shear/isotropic/cap-active manufactured stress, and closed/default-three/converged-200 branches. This is the frozen GPU matrix, not the expanded 736-case host matrix. Outputs `baseline.json` and `patched.json` contain raw scalar omega values and device name. Completion means only the diagnostic executed; an independent comparison against the declared host oracle is still required. A failed run leaves partial evidence for inspection and must not be overwritten.
|
||||
|
||||
## Interpretation and limits
|
||||
|
||||
Full-stress versus deviatoric-stress selection, iterative viscosity cap, iteration count/relaxation, omega guards, collision dispatch, and AMR guards remain unchanged. Closed and iterative paths intentionally remain different for isotropic or cap-active stresses. No settings or Cs defaults change. High stress is a helper diagnostic, not an admissible flow-state claim.
|
||||
|
||||
The corrected scalar formula is justified only under the stated stress-strain estimate. MRT full-trace stress does not generally share one shear relaxation rate, so its bulk coupling remains uncertified. Force-corrected non-equilibrium stress and Guo-force interaction also remain uncertified. These manufactured tests are unforced and do not cover full TRT/MRT collision, streaming modes, shifted storage, walls/Jets, flow solutions, or 3D turbulence. Existing guard statements are preserved, not newly comprehensively tested; the main matrix is guard-inactive. No AMR LES support is added or enabled.
|
||||
|
||||
Production integration and removal of this temporary candidate fixture are separate decisions after GPU and applicable flow evidence. CPU success alone does not approve a production change.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Moving circles — stage-1 evidence (development level)
|
||||
|
||||
Owner criterion (Yanqi via TRON): prescribed motion and one free VIV case both
|
||||
"reasonably close" to a predeclared reference = development-stage success.
|
||||
This is practical closeness, not a formal validation claim. Every number
|
||||
below is from Astra's receipts as posted in Slack C0C78UFKGRM thread
|
||||
1793335557.460509; raw outputs stay on Horsea under `/tmp/celeris-results-*`
|
||||
and the Mac evidence directory (not in git).
|
||||
|
||||
## Code
|
||||
|
||||
- Production: `src/` identical to `653fd997` (slices S1 `983020b6`,
|
||||
S2a `b920a303`, S3a `653fd997`). CPU suites and GPU hold/crossing gates
|
||||
passed on those slices (G0a/G0b population-bitwise; X1 crossing ledger).
|
||||
- Research drivers: `research/duetsch_re100_kc5/` (`dd37883d`, capture
|
||||
`a0aea3b9`) and `research/viv_re150/` (`fb507aa1`), run as committed.
|
||||
|
||||
## Prescribed in-line oscillation (Dütsch et al. 1998, Re 100, KC 5)
|
||||
|
||||
- Protocol: `docs/validation/duetsch_re100_kc5_protocol.md`.
|
||||
- Frozen settling gate (.002 F_ref waveform): **FAILED** and kept on record:
|
||||
D40 domain stage (20/40/60 D, 11.25 T) and the 60 D duration diagnostic
|
||||
(31.25 T, 0/28 pairs; last waveform drift .00613 F_ref).
|
||||
- Unscored late-cycle CME fits c_d ≈ 2.02–2.05, c_i ≈ 1.46–1.48 vs the
|
||||
paper's computed (2.09, 1.45); fields clean and symmetric (owner view).
|
||||
The owner accepts ~0.61 % cycle drift at D40 for this stage; the gate is
|
||||
not re-scored.
|
||||
- Fig. 8 LDA profiles: qualitative only (no digitized ordinates).
|
||||
|
||||
## Free transverse VIV (Kolahdouz §4.1.1 setup, Qin §4.5 target)
|
||||
|
||||
- Predeclaration: `docs/validation/viv_re150_predeclare.md` (agreed at
|
||||
`1abfe954` under its former name `leontini_viv_predeclare.md`); run at `fb507aa1`, single case, D = 40, 75 D × 60 D.
|
||||
- Result: status `scored`, 336,384 steps (t U/D = 336.4); A_max = .5464 D vs
|
||||
.520 D (+5.1 %, inside the declared ±10 % band); mean half peak-to-peak
|
||||
.5448 D; f/f_n = .955; block drifts .78/.29/.18 %, last-five spreads
|
||||
.15/.11/.10 %; finite, no_donor = 0, no seed; 337.7 s, ~1.0 ms/step.
|
||||
|
||||
## Limitations (binding)
|
||||
|
||||
- One resolution (D40) per case; no grid or time convergence.
|
||||
- Structural RHS = CME + Wen is a theory-motivated stage choice; CME-only
|
||||
and node-impulse variants were not run as RHS; no force law is validated.
|
||||
- Boundary mismatches: NEQ outlet with backflow clamp vs traction outlet;
|
||||
finite box; the strict Dütsch settling failure is unexplained.
|
||||
- No mass correction at curved walls; ~1 ms/step host-coupled overhead.
|
||||
@@ -1,70 +0,0 @@
|
||||
# Matched SRT uniform / refined DFG Re100 preparation
|
||||
|
||||
Status: CPU-preflight checked; **not GPU executed or approved to execute**. No physical acceptance or speedup claim. No production source changes. Root owns GPU admission and integration.
|
||||
|
||||
## Frozen setup
|
||||
|
||||
Canonical source snapshot: integrated commit `cd3a657f9d3833e812c3cca173525f165b48710a`. Production origin remains candidate `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5` (on `5338864`, on `69e9b78`); production files are byte-identical. The integrated Re100 helper header/PIN records that origin explicitly. Both commit identities are retained in the manifest. `tests/validation/data/refined_dfg/source_manifest.json` hashes all production source files plus the existing DFG geometry/pressure helper, periodicity helper and official reference cycle. An integrated checkout may have a different commit but must have identical pinned file contents. No MRT external-reference run is reused as the matched uniform control.
|
||||
|
||||
Both cases: SRT, shifted FP32, double buffer, no LES, no applied force, stationary cylinder, local Zou–He parabolic inlet, halfway y walls, identical pressure-NEQ outlet with **all nine nonequilibrium components damped by 0.5**, backflow clamp disabled. This outlet is not the exact canonical DFG do-nothing condition. Raw cylinder reaction observation is a diagnostic, not applied forcing.
|
||||
|
||||
Physical units use D=0.1 m, mean U=1 m/s, ν=0.001 m²/s, density reference=1; domain 2.2×0.41 m, cylinder center (0.2,0.2) m. Uniform D80: 1761×330, dx=0.00125 m, dt=0.0000125 s, ν_lattice=.008. Refined root D40: 881×166, dx=.0025 m, dt=.000025 s, ν_lattice=.004; fine dx/dt/ν match uniform D80. All lattice mean speeds are .01.
|
||||
|
||||
Fixed half-open patch **(40,24,320,140)** in root edge coordinates maps to x=[.09875,.79875] m, y=[.0575,.3475] m. The body and about six diameters of downstream wake are fine; wall layers and far wake remain coarse. Bounds are frozen before outcomes, not tuned afterward. Geometry-only leaf-count and nominal collision-work ratios to uniform are ~.419 and ~.321. These omit real launch/transfer/halo/measurement costs and are not measured speedups. The narrow root block height is legal but potentially expensive; do not infer speed from leaf count.
|
||||
|
||||
Exact root dimensions are tiled by (by,bx)=(2,881); no padding. Fine allocated shape (240,568), including four-cell halos, uses (8,8). `host_geometry` calls production channel transfer-overlap checks and complete fine cut-link/donor/transfer guards before any CUDA import.
|
||||
|
||||
### Half-cell conversion
|
||||
|
||||
Uniform integer nodes have physical coordinates (i*dx, (j-.5)*dx). Root edge coordinates therefore map to ((X-.5)*dx_root, (Y-1)*dx_root). Body edge center=(80.5,81), radius=20; wall edges Y=1,165; source planes X=.5,880.5. Fine local body nodes are `(body_edge - fine_origin)/.5 - .5`.
|
||||
|
||||
Body and physical wall planes match exactly. The axes have different phases. With fine origin O=(38,22):
|
||||
|
||||
- x_f = [O_x+(j+.5)/2-.5] dx40 = (j+75.5) dx80, versus x_uniform = i dx80. Fine x samples have a half-fine-cell (quarter-root-cell) offset.
|
||||
- y_f = [O_y+(k+.5)/2-1] dx40 = (k+42.5) dx80, versus y_uniform = (l-.5) dx80. The y samples align exactly for l=k+43.
|
||||
|
||||
This difference follows from the inlet's ghost-node plane versus the half-way y-wall planes. The host phase test checks both mappings. Compare interpolated observables, not pointwise arrays or bitwise equality; x-phase error, the different fine collision arithmetic, wall resolution, and coarse/fine transfer effects all contribute to discrepancies. No field-difference acceptance is claimed by this runner. Initial fluid state samples the same analytic parabolic equilibrium as uniform production initialization; root boundary ghosts start at rest. It is not asserted to be bitwise identical across different grids/arithmetic.
|
||||
|
||||
## Observations and gates
|
||||
|
||||
- Every AMR base step is individually followed by `read_raw_exchange(..., window='last_base_step')`: two fine substeps, root-lattice per-unit-depth impulse integrated over [n,n+1]. `step(n)` alone retains only its last base interval, so it cannot support averaging the whole batch.
|
||||
- Uniform `Simulation.run(batch)` / `read_force` gives the mean over that batch; multiply by batch before accumulating. Cd/Cl divide the interval mean reaction by 0.5 U_lattice² D_lattice in each case. Force unit is ρ dx³/dt² per depth, so fine-uniform lattice force is twice root force for the same physical load.
|
||||
- Force timestamps are observation-interval midpoints. Pressure uses physical decoded postcollision density at the interval end, extrapolated from fluid-only points one/two **fine** cells outside the cylinder using existing `pressure_probes`. Pressure difference is normalized by ρ_ref U_physical². Existing periodic helpers align pressure onto force cycles.
|
||||
- Public AMR downloads exclude nonleaves. Fixed geometry masks, not state-dependent finiteness masks, check all fluid populations and sum ρ dx_level²; covered root cells, body, boundaries and fine halos are excluded. Corrupt NaN fluid cannot disappear from the mass check. Mass is per-unit-depth ρ_ref·m². Uniform uses its physical fluid flags and dx². Different sampling of the circular boundary means initial discrete fluid volume can differ slightly; compare temporal stability, not identical absolute mass.
|
||||
- Flux samples root/uniform x-index 2 and -3, summed with dx and physical velocity conversion. Those planes differ by grid-scale offsets; cycle-average inlet/outlet balance is the gate, not direct pointwise flux equality.
|
||||
- Dense measurement width Δt*=.02 in the pilot and for t*≥240, otherwise .5. Pilot ends t*=5; full ends t*=300. Same existing five-cycle periodicity, mass, flux, and reference gates are imported without modification. Both official-reference diagnostics and refined-versus-uniform cycle comparisons are written. `matched_case_pass` refers only to matched SRT agreement, not certification of canonical DFG or the broader suite.
|
||||
- Safety stop: nonfinite state/reaction, Mach>.2, or density outside [.5,1.5]. GPU durations include required exchange reads and synchronization in stepping; full wall time also includes host measurements. Do not report pure-kernel throughput from these numbers.
|
||||
|
||||
## Commands
|
||||
|
||||
From the repository root, CPU only:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python tests/unit/test_refined_dfg.py
|
||||
python tests/validation/run_refined_dfg.py --mode plan --output /tmp/refined-dfg-plan-new
|
||||
```
|
||||
|
||||
Fourteen CPU tests are retained in tests/unit/test_refined_dfg.py. Plan JSON is configuration output only; no preparation result files are committed. The initial attempt to use pytest was unavailable in this interpreter; tests use standard-library unittest and NumPy instead.
|
||||
|
||||
Only after root channel GPU gates pass and root explicitly admits an idle physical GPU, root may run:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <caller-hard-budget>s python tests/validation/run_refined_dfg.py --mode pilot --gpu-gates-approved --max-wall-seconds <caller-soft-budget> --output <new-pilot-directory>
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <caller-hard-budget>s python tests/validation/run_refined_dfg.py --mode full --gpu-gates-approved --max-wall-seconds <caller-soft-budget> --pilot-results <completed-pilot-directory> --output <new-full-directory>
|
||||
```
|
||||
|
||||
`--gpu-gates-approved` is a caller attestation, not proof that GPU tests ran. Full mode additionally requires both matched healthy pilots through t*=5 with identical source hashes, bounds and runner hash. No GPU/Mac/remote operation has been performed for this preparation. Pilot/full outputs must be new/empty. Uniform runs first; an unhealthy case stops the campaign. A full run that fails periodic/matched gates exits nonzero.
|
||||
|
||||
## Bounded execution and receipt validation
|
||||
|
||||
No pilot throughput is known yet; the caller must set a finite positive `--max-wall-seconds` rather than treating an invented estimate as calibration. After valid pilots, the full-run calibrated budget is 3 × sum_over_cases[startup_seconds + 60 × (pilot_wall_seconds − startup_seconds)]. The full run uses the smaller of that estimate and the caller's explicit ceiling. This uses total measured pilot evolution/observation cost and the full/pilot physical-time ratio (300/5); 3× is a declared safety margin, not a runtime guarantee. The plan records both budgets.
|
||||
|
||||
The campaign deadline begins before the first case initialization. Cooperative checks surround initialization, every AMR root step or uniform ≤100-step batch, and measurements. On observed expiry, the runner stops, writes a nonpassing `wall_time_limit` receipt, and exits 3. A blocking CUDA call cannot be interrupted by these checks: the external `timeout` watchdog is required, with a hard budget chosen slightly above the soft budget to permit cleanup. Hard termination may leave partial CSV data and no final receipt; that never qualifies as a completed pilot.
|
||||
|
||||
Each case emits `READY` with strict finite JSON and saves `ready.json` only after its first healthy complete observation, after initialization, actual stepping, force readback, macro/pressure/mass/flux paths and CSV flush all succeeded. No early initialization-only READY is emitted.
|
||||
|
||||
Full-run pilot validation rejects nonfinite JSON constants and numeric overflow, wrong case/mode/status, nonboolean health, missing/duplicate/out-of-order rows, wrong step counts or observation times, missing numeric observations, health-limit violations, absent/mismatched READY receipts and invalid timing calibration. It requires exactly 250 observations spanning the full dense t*=5 pilot, source/runner hashes and frozen bounds. Tests include the reviewer's exact NaN-endpoint regression and malformed receipt variants. Synthetic test fixtures exist only in temporary directories and are not simulation results.
|
||||
|
||||
## Integrated source import guard
|
||||
|
||||
The runner's source defaults to this repository root. Before geometry or GPU work, it verifies the pinned source/helper/reference contents and requires the imported CelerisLab package to be exactly `<source>/src/CelerisLab/__init__.py`. An installed DynamisLab copy or another cached checkout is rejected. Integration changes paths/readability and this explicit import assertion; geometry, numerical definitions, gates, and receipt validation remain frozen. Any new path-formatted runner has its own hash, so old preparation pilot receipts cannot authorize a full run of this integrated runner.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Matched MRT DFG pilot preparation
|
||||
|
||||
Preparation only: this test-only extension reuses the existing runner and existing MRT
|
||||
operator. GPU admission awaits reviewed MRT capability results; no pilot/full execution
|
||||
is implied by CPU preflight. No production, historical SRT manifest, reference data,
|
||||
periodic gate, geometry, initialization or observation algorithm is changed here.
|
||||
|
||||
## Selection and provenance
|
||||
|
||||
Use `--collision MRT` for **both** uniform D80 and refined root D40/fine D80. There are
|
||||
no independently selectable collision models per case. Default remains SRT.
|
||||
|
||||
MRT verifies `data/refined_dfg/source_manifest_mrt.json`, pinned to reviewed numerical
|
||||
source snapshot `3ea131e92c057843034f11a1c574301a2430dfce`, including the shared geometry,
|
||||
pressure/cycle helpers and official reference. Test-only commits may change HEAD without
|
||||
changing this numerical source pin. Plan, READY, case summaries and comparison separately
|
||||
record `checkout_revision` and `checkout_dirty` using existing source-provenance helper;
|
||||
the runner hash remains separate. Collision is recorded in every receipt, and full-mode
|
||||
validation rejects missing or different collision labels in plan, either summary or either
|
||||
READY receipt, in addition to existing source/runner/health/time checks.
|
||||
|
||||
The historical SRT manifest remains byte-for-byte unchanged. SRT on the MRT candidate
|
||||
checkout deliberately fails that source pin; use a matching historical checkout via
|
||||
`--source` for SRT. No fallback or automatic repinning is permitted. Historical SRT
|
||||
failure/diagnostic evidence remains attached to its original source and runner hashes;
|
||||
it cannot authorize an MRT full run. The existing failed-observation step-reporting
|
||||
undercount is not changed in this narrowly scoped selection patch.
|
||||
|
||||
## Frozen physical comparison
|
||||
|
||||
All settings remain those documented in `refined_dfg.md`: U_lattice=.01, physical mean
|
||||
U=1 m/s, D=.1 m, shear ν=.001 m²/s, fixed physical domain/body/wall planes, patch
|
||||
(40,24,320,140), shifted FP32 double buffers, no LES/forcing/Jets, finest dx=.00125 m,
|
||||
finest dt=.0000125 s, unchanged half-cell frame conversion and axis-specific phase.
|
||||
Force interval accumulation, physical leaf mass, pressure interpolation, flux sampling,
|
||||
safety thresholds, t*=5 pilot and t*=300 cap are unchanged. Existing accuracy/periodicity
|
||||
criteria are imported unchanged. Both MRT cases are fresh runs; old MRT or SRT results
|
||||
are not substituted as controls.
|
||||
|
||||
Both cases use the **existing MRT pressure-NEQ branch**: only unknown directions q2/6/8
|
||||
are extrapolated. This differs from SRT's all-nine .5-damped branch; the retained
|
||||
`srt_neq_damp=.5` configuration value is inactive under MRT. Backflow clamp remains off.
|
||||
The outlet is still noncanonical relative to exact DFG do-nothing conditions.
|
||||
|
||||
The reused MRT operator keeps fixed kinetic rates `s_e=s_eps=s_q=1.2` on all levels;
|
||||
only the shear rate follows existing per-level viscosity. Physical bulk relaxation
|
||||
therefore differs between coarse and fine levels. Transfer operators and subcycling
|
||||
are unchanged. This is the candidate's declared behavior, not a new fully mode-scaled
|
||||
MRT transfer scheme. Do not tune rates, the patch or gates after seeing outcomes.
|
||||
Capability smoke/parity gates do not establish DFG accuracy or speedup.
|
||||
|
||||
## CPU and admission commands
|
||||
|
||||
From this checkout:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --mode plan --output /tmp/new-mrt-dfg-plan
|
||||
```
|
||||
|
||||
After root verifies the candidate MRT root/parity/refinement capability results and admits
|
||||
an idle device, the paired pilot may use the existing bounds and external watchdog:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/run_refined_dfg.py --collision MRT --mode pilot --gpu-gates-approved --max-wall-seconds <soft-budget> --output <new-mrt-pilot-directory>
|
||||
```
|
||||
|
||||
The flag is caller attestation, not evidence. An unhealthy first case prevents the second.
|
||||
Existing strict pilot receipt validation and calibrated wall-time controls remain; full
|
||||
mode is not being admitted as part of this preparation. Root must review both healthy
|
||||
pilot outcomes and separately decide any full-run admission.
|
||||
@@ -1,58 +0,0 @@
|
||||
# Refined DFG MRT timing harness (measurement only)
|
||||
|
||||
`tests/validation/time_refined_dfg.py` times the matched MRT refined/uniform DFG Re100 setup of
|
||||
`run_refined_dfg.py` (same configuration, geometry and initial state functions). No numerical,
|
||||
physics-gate or production change; no force or accuracy result is produced.
|
||||
|
||||
| Variant | Loop | Force reads |
|
||||
|---|---|---|
|
||||
| A | refined `step(1)` + `read_raw_exchange` per base step | runner-like force-read workload (values discarded) |
|
||||
| B | refined `step(1)` only, no host reads (`observe_raw_exchange` stays on) | timing-only |
|
||||
| C | refined single `step(N)` | timing-only; ledger keeps the final base interval only |
|
||||
| E | uniform `sim.run(<=100)` + `read_force` | runner-like force-read workload (values discarded) |
|
||||
| E' | uniform single `sim.run(2N)` | timing-only; at most a whole-window average, different summation order |
|
||||
|
||||
- No variant retains a valid force history; A/E omit the runner's interval aggregation and
|
||||
validation, and no variant includes macro/probe/CSV costs. Results are not full benchmark
|
||||
throughput.
|
||||
- Status lines: `CONSTRUCTED` (construction/JIT complete) and `WARM_READY` (first warm-up and
|
||||
health check passed) per case.
|
||||
- Equal simulated duration: N refined base steps vs 2N uniform D80 steps.
|
||||
- Every repeat starts from the same fresh state (refined `initialize`, uniform `snapshot/restore`)
|
||||
and the same warm-up; construction/JIT, warm-up, downloads and parity checks are untimed.
|
||||
- The actual stepping stream is synchronized explicitly before and after each timed loop; the
|
||||
wall timer (`time.perf_counter`) is the only source. No CUDA-event span is called kernel time.
|
||||
- `--max-wall-seconds` is required for GPU runs (outer timeout, checked between variants and
|
||||
every 100 steps in per-step loops).
|
||||
- Final populations and NaN masks must be bitwise identical to the first A repeat (A/B/C) or the
|
||||
first E repeat (E/E'); refined and uniform grids are never compared. Leaf/fluid populations
|
||||
must be finite. Physical health (finite leaf/fluid macros, rho in [0.5, 1.5], max Mach <= 0.2,
|
||||
the existing runner limits) is checked after warm-up and after each timed window. Matching NaN
|
||||
is accepted only as a storage mask; any infinity or level-count mismatch fails parity. All
|
||||
parity/health checks are untimed.
|
||||
- Cases run sequentially (uniform, then refined) in one process with startup serialized;
|
||||
variants are interleaved within each repeat. A-B and B-C are measured comparative
|
||||
differences under GPU variability, not exact causal decompositions.
|
||||
|
||||
Usage (root selects one verified idle GPU):
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_refined_dfg.py
|
||||
python tests/validation/time_refined_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_refined_dfg.py --output <new> --max-wall-seconds <s>
|
||||
|
||||
Defaults: 2000 base steps, 200 warm-up base steps, 3 repeats. Profiler capture and the
|
||||
`observe_raw_exchange=False` variant are deferred until these measurements exist.
|
||||
|
||||
## A versus observation-window ledger (`tests/validation/time_window_dfg.py`)
|
||||
|
||||
Measurement-only comparison on the same shifted-MRT refined setup: A (per-base `step(1)` +
|
||||
`read_raw_exchange`, summed per 80-base-step interval), W (`start_exchange_window`, `step(80)`,
|
||||
one window read) and W1 (window with 80 x `step(1)`, isolating the read pattern from call
|
||||
batching). Same fresh state, warm-up and simulated duration; populations must be bitwise equal
|
||||
to the first A repeat and per-interval total impulses within the documented engineering
|
||||
tolerance, all outside timing. The candidate source is recorded (checkout revision, dirty flag,
|
||||
file hashes); frozen refined-DFG manifests/results are not used or changed.
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_window_dfg.py
|
||||
python tests/validation/time_window_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_window_dfg.py --output <new> --max-wall-seconds <s>
|
||||
@@ -1,117 +0,0 @@
|
||||
# Opt-in observation-window ledger in the DFG runner
|
||||
|
||||
This test-only integration adds `--exchange-window` to `run_refined_dfg.py`. It uses the
|
||||
existing production ledger; no collision, boundary, transfer, physical setup, reference,
|
||||
periodicity gate or acceptance threshold changes. Default remains the historical
|
||||
per-base-step observation path. Uniform stepping/force accumulation is unchanged.
|
||||
|
||||
For the refined case, each existing observation interval opens a new ledger at completed
|
||||
base step `begin`, advances exactly the same `n` base intervals, and reads the integrated
|
||||
root-lattice impulse for `[begin, begin+n]`. The contract requires root units, `window`
|
||||
kind, `2*n` fine samples, matching endpoints, and fine dt=.5. The existing coefficient
|
||||
calculation still divides this impulse by `n` and `.5*U_lattice**2*D_root`; force timestamps
|
||||
remain `(begin+n/2)*U_lattice/D_root`. It does not divide an already averaged force again.
|
||||
Pressure/macro/physical-leaf mass measurements and safety checks remain at the original
|
||||
interval end. Pilot interval is 80 root steps; early full-run intervals remain 2000, and
|
||||
any final shortened interval uses its actual `n`.
|
||||
|
||||
The opt-in uses one `step(n)` call and one window read per observation, rather than `n`
|
||||
Python calls/readbacks. Internal subcycling and per-base synchronization remain owned by
|
||||
the same backend. Cooperative deadline checks surround that complete interval; unlike
|
||||
the default path they cannot interrupt between base steps inside `step(n)`. The existing
|
||||
external hard watchdog is still required. The full-run cadence/cap is documented here,
|
||||
but this preparation does not admit a full run or a GPU operation.
|
||||
|
||||
## Provenance and receipt separation
|
||||
|
||||
`source_manifest_window.json` pins the reviewed a233c9032487a6006e950d1e327079b00466c410
|
||||
snapshot (production ledger origin d930) plus existing helper/reference files. Both SRT
|
||||
and MRT are supported by this explicit opt-in source pin; collision selection remains
|
||||
independent and shared by the two cases. Existing SRT and MRT manifests are untouched.
|
||||
Default mode continues to reject the changed candidate under those historical pins;
|
||||
use a matching historical `--source` checkout to run its default path. There is no
|
||||
silent repinning, source fallback, or reuse of historical outputs as candidate evidence.
|
||||
|
||||
Plan, READY, summaries, timeout/comparison receipts now record `observation_mode` as
|
||||
`last_base_step` or `window`, alongside collision and actual checkout provenance. On a
|
||||
uniform receipt this labels its **paired campaign's refined observation mode**, not a
|
||||
change to uniform observation. Full-mode pilot validation rejects missing or mismatched
|
||||
mode labels in the plan, either case summary, and either READY receipt. Source/runner
|
||||
hash, collision, completeness and health checks remain. Cross-mode pilot reuse is denied.
|
||||
|
||||
The earlier short A/W/W1 timing/capability evidence is not a full-run speedup or accuracy
|
||||
claim. Mathematical impulse integration/units are unchanged; floating-point reduction
|
||||
order can differ, so bitwise equality of force sums is not promised. Physics acceptance
|
||||
continues to use the existing frozen gates. Historical failures/results are untouched.
|
||||
|
||||
CPU preparation commands, from the repository root:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
PYTHONPATH=src python -m unittest tests.unit.test_raw_exchange_window tests.unit.test_time_window_dfg -v
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --exchange-window --mode plan --output /tmp/new-window-dfg-plan
|
||||
```
|
||||
|
||||
Root must independently review and admit any execution. An admitted paired pilot uses
|
||||
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
||||
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
|
||||
selected collision's pilot command. No GPU or remote action was performed for this patch.
|
||||
|
||||
## Public Jet diagnostic option
|
||||
|
||||
`--collision MRT --exchange-window --jet-scenario zero|fixed|reverse` selects
|
||||
unshifted FP32, no LES, constant transfer on both existing public backends. Default
|
||||
`none` and the historical manifests remain unchanged. `zero` registers both Jets at
|
||||
zero amplitude and retains every existing DFG verdict, including pressure. `fixed`
|
||||
uses outward radial parabolic sectors at pi/2 and 3pi/2, half-width pi/12, peaks
|
||||
(+.001,-.001). Both use the existing D40/D80 geometry and U=.01. Sector origins are
|
||||
(80.5,81) in root edge coordinates and (160,160.5) in uniform node coordinates;
|
||||
radial extent is [0,1.005R]. `reverse` keeps those peaks through t*=300, updates both
|
||||
public setters at that completed boundary, and continues to 305 with opposite signs.
|
||||
|
||||
Fixed diagnostic windows are 250–260, 260–270, 270–280, 280–290 and 290–300; reversal
|
||||
adds 300–305. Histories retain exact interval endpoints and root/uniform lattice
|
||||
impulses. The refined window uses total exchange; uniform uses public `read_force`
|
||||
batch averages. Driven windows report mean/min/max/RMS Cd/Cl without applying DFG
|
||||
reference or periodicity gates. Exit zero means healthy execution, not qualification;
|
||||
`matched_case_pass` remains false. No independent CV or stress-load claim is made.
|
||||
|
||||
This opt-in records current source/helper hashes with the existing manifest format,
|
||||
checkout revision/dirty state and runner hash. Pilot/full must match that snapshot
|
||||
and the exact scenario. It does not relabel current source as historical evidence.
|
||||
Use the existing bounded plan/pilot/full commands with the three option flags above;
|
||||
full requires the matching completed t*=5 paired pilot. `step_seconds` includes force
|
||||
readback; observation/overhead and total wall time are separate, not kernel throughput.
|
||||
|
||||
Add `--observe-cv` to record independent scalar control-volume samples from the
|
||||
already downloaded public macros. The pilot exercises this path at t*=.02..5;
|
||||
full records 501 endpoints at 290..300. Each `cv_history.jsonl` row contains actual
|
||||
root time, preceding interval start/duration/impulse, and two inventories/outward
|
||||
fluxes ordered [mass,Px,Py]. Exclude the first row's preceding impulse: that row
|
||||
supplies the initial inventory. Uniform native impulse converts by .5² to root
|
||||
units; refined ledger impulse is already in root units. Explicit native-aligned
|
||||
contours differ by .25 root cells in x and are recorded in the plan. Cropping
|
||||
retains the two-cell face-gradient stencils. No fields or per-link arrays are saved.
|
||||
`cv_observation_seconds` is the CV portion of observation/overhead time. Helper hash
|
||||
and the observation flag must match pilot/full. Time integration, temporal
|
||||
coarsening and any physical assessment remain separate offline work.
|
||||
|
||||
## Bounded resolution and placement controls
|
||||
|
||||
Defaults remain `--case both --uniform-diameter 80 --placement wide`. Single-case
|
||||
runs use `--case uniform` or `--case refined` and never report a matched-case pass.
|
||||
Their own reference/periodicity or driven-diagnostic results remain available.
|
||||
A single zero-Jet full run exits successfully only when its periodicity and the
|
||||
unchanged complete DFG reference verdict pass; driven completion is not qualification.
|
||||
|
||||
The D160 option requires `--case uniform --uniform-diameter 160` and an explicit
|
||||
public Jet scenario. U and Jet speeds stay .01 and ±.001; viscosity is .016,
|
||||
grid 3521×658, center (320,320.5), radius80. CV uses root spacing/time factor .25,
|
||||
origin (.375,.75), impulse factor .0625, root viscosity .004, and x-face shift .125.
|
||||
|
||||
`--case refined --placement compact` selects root bounds (48,48,128,128), fine
|
||||
168×168 and origin (46,46), with the same D40/fineD80 body/Jet resolution. Its CV
|
||||
contours are (52,52,112,112) and (50,50,120,120); complete face-gradient stencils
|
||||
are clear of both solids and the transfer footprint. Compact is refined-only.
|
||||
CV metadata gives each grid's spacing/origin/contours explicitly. Case, uniform
|
||||
diameter and placement must match the newly completed pilot in every receipt.
|
||||
@@ -1,161 +0,0 @@
|
||||
# Periodic 2D Taylor–Green AMR campaign (prepared, not GPU-qualified)
|
||||
|
||||
This validation harness complements DFG Re100 and Jet/body tests. It measures
|
||||
smooth 2D periodic decay and errors crossing fixed refinement interfaces at
|
||||
Re_L=100 and 1000. It does **not** certify 3D turbulence, LES, Jet performance,
|
||||
wall conditions, dynamic refinement or general high-Re flows.
|
||||
|
||||
## Reference and conventions
|
||||
|
||||
Primary implementation-author documentation, checked 2026-10-18:
|
||||
- TensorMesh, exact velocity and pressure with a consistent phase:
|
||||
https://docs.tensor-mesh.com/example_gallery/fluid/taylor_green.html
|
||||
- AeroSim Nassu, 2D decay, energy and lattice-length Re definition:
|
||||
https://docs.aerosim.io/nassu/validation/analytical/04_taylor_green_vortex_2d/README.html
|
||||
|
||||
We use the TensorMesh phase, rescaled to length L=N root cells:
|
||||
|
||||
k = 2*pi/L; A(t) = U*exp(-2*nu*k*k*t)
|
||||
ux = -A*cos(k*x)*sin(k*y)
|
||||
uy = A*sin(k*x)*cos(k*y)
|
||||
p/rho0 = -A*A/4 * (cos(2*k*x)+cos(2*k*y))
|
||||
E = <|u|^2>/2 = U*U/4 * exp(-4*nu*k*k*t)
|
||||
|
||||
Pressure is kinematic, with rho0=1. A global velocity sign reversal leaves
|
||||
pressure unchanged; exchanging sine/cosine without the corresponding pressure
|
||||
phase can reverse its sign. CPU tests independently check momentum residuals.
|
||||
The nonlinear acceleration is a pressure gradient, **not zero**; the AeroSim
|
||||
page's prose calling advection zero is not adopted. Do not mix phase variants
|
||||
from different references. The formulas above are verified by substitution,
|
||||
not by trusting a copied pressure expression.
|
||||
|
||||
Re_L=U*L/nu, not Re_k=U/(nu*k)=Re_L/(2*pi). U=.03 in lattice units,
|
||||
Ma=U/cs=.0519615, cs^2=1/3, nu_root=.03*N/Re_L. Acoustic refinement uses
|
||||
h_l=dt_l=2^-l: lattice velocity stays U, nu_l=2^l*nu_root and
|
||||
tau_l=.5+3*nu_l. The worst root tau is .50288 (N32/Re1000), omega=1.98855,
|
||||
inside the existing config guard. No viscosity clamping or guard override.
|
||||
|
||||
All initial arrays are physical FP64 equilibrium populations, supplied to the
|
||||
public shifted-FP32 postcollision-G initializer. rho=1+3p at each true cell
|
||||
center. These are valid equilibrium postcollision states, but do not include
|
||||
Chapman–Enskog nonequilibrium stress; startup acoustic/kinetic error remains in
|
||||
the measurements and is shared with uniform baselines. The incompressible
|
||||
reference is not an exact finite-Mach LBM solution. This is why resolution alone
|
||||
need not remove the full error floor.
|
||||
|
||||
## Geometry and workload
|
||||
|
||||
All patches use level-wide union form, so origins are zero, including fine
|
||||
arrays with holes. Coordinates are (index+.5)*2^-level. Metrics integrate only
|
||||
ACTIVE leaves, each weighted by 4^-level. Covered cells and halo cells never
|
||||
enter norms or mass. Geometry tests prove exact single coverage at root-cell
|
||||
resolution. All blocks are 8x8, and both root dimensions are periodic.
|
||||
|
||||
At each root N=32,64:
|
||||
- 2 levels: L1 rectangle (N/8,N/8,7N/8,3N/4), in L0 indices.
|
||||
- 3 levels: same L1 plus L2 rectangle (3N/4,N/2,3N/2,5N/4), in L1 indices.
|
||||
|
||||
These off-axis boundaries cross nonzero flow and gradients rather than sitting
|
||||
only on symmetry or stagnation lines. Whole geometry scales with N.
|
||||
|
||||
16 cases: at each Re, uniform N=32,64,128,256 plus AMR (root32/root64)x(depth2/depth3).
|
||||
Each AMR case therefore has both a same-root uniform baseline and a uniform
|
||||
baseline at its finest spacing. Uniform N is its own lattice length, so nu and
|
||||
step count are scaled to represent the same nondimensional solution.
|
||||
Samples t*=tU/L = 0,.12,.24,.36,.48. End time is 16*N base steps, a meaningful
|
||||
.48 domain turnover or 3.016 vortex-scale turnover times (scale 1/k).
|
||||
At Re1000 energy decays about 7.3%; at Re100 about 53.1%. No plots or full fields;
|
||||
each case writes five diagnostic rows and provenance, incrementally.
|
||||
|
||||
## Running (GPU owner must schedule launches)
|
||||
|
||||
CPU checks, from the repository root:
|
||||
|
||||
PYTHONPATH=src python -m unittest discover -s tests/validation/taylor_green -p 'test_*.py' -v
|
||||
|
||||
One GPU case, on the scheduled worker after checking occupancy:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-id> python tests/validation/taylor_green/run_case.py --repo /path/to/reviewed/checkout --re 100 --n 32 --depth 1 --output output/taylor_green/re100_n32_d1.json
|
||||
|
||||
Depth 1 is uniform. Run uniform cases before AMR for each Re; inspect baseline
|
||||
screening failures before proceeding. For a complete matrix, repeat the uniform
|
||||
call with N=32,64,128,256 and AMR calls with N=32,64 / depth=2,3, at each Re.
|
||||
Use serial process startup on the parent's assigned Horsea GPUs. This harness
|
||||
does not schedule or acquire GPUs. No GPU was used during preparation.
|
||||
|
||||
python tests/validation/taylor_green/assess.py output/taylor_green/re*_n*_d*.json --output output/taylor_green/assessment.json
|
||||
|
||||
The local API/geometry review used clean staging commit
|
||||
cd3a657f9d3833e812c3cca173525f165b48710a. This is **not** a claim of results on d79
|
||||
or any historical commit. Each runtime output records actual Git HEAD/dirty
|
||||
state, an aggregate source-tree hash, plus SHA256 of all harness Python files. Assessment refuses dirty checkouts and mixed source
|
||||
or harness identities. Keep reports outside the checkout or .gitignore them so
|
||||
new outputs do not change its dirty flag between cases. The source-tree hash excludes bytecode caches; production qualification requires
|
||||
a clean checkout.
|
||||
The runner uses the existing PeriodicBlockStepper / FixedPatchStepper periodic
|
||||
paths only. It does not depend on the new channel-root implementation.
|
||||
|
||||
## Preregistered assessment, before any GPU results
|
||||
|
||||
All sampled times must satisfy finite screening ceilings:
|
||||
- Relative composite velocity L2 <= 5%
|
||||
- Relative globally gauge-adjusted pressure L2 <= 15%
|
||||
- Relative kinetic-energy error <= 5%
|
||||
- Composite mass drift from downloaded t0 <= 1e-5
|
||||
|
||||
These are screening budgets, not literature-certified tolerances. Acceptance
|
||||
also requires all 16 cases, common source/harness, correct nondimensional times
|
||||
and viscosity, and baseline-relative checks on worst post-startup-sample errors:
|
||||
- AMR error <= max(2 * same-root uniform error, declared numerical floor).
|
||||
- Root64 AMR error <= max(.9 * root32 AMR error, floor), for each depth/Re.
|
||||
- Uniform256 error <= max(.9 * uniform32 error, floor).
|
||||
|
||||
Floors: velocity .002, pressure .02, energy .004. They are declared low-Mach /
|
||||
equilibrium-startup budgets, not fitted to observed results. Ma^2=.0027 motivates
|
||||
their scale; it is an asymptotic expectation, not a proof of those coefficients.
|
||||
The coarse baseline sets the AMR budget because much of the domain remains
|
||||
coarse. The finest uniform error and AMR/fine ratio are always reported, with no
|
||||
claim that partial refinement should match a fully fine domain. A pass below a
|
||||
floor is bounded accuracy, not a measured convergence order. Report errors and
|
||||
ratios even for a pass. Failure requires diagnosis or a separately preregistered
|
||||
follow-up, never retroactive tolerance tuning. Higher Re is deliberately absent;
|
||||
Re100/1000 must first be stable, resolved and reviewed.
|
||||
|
||||
Diagnostics use one global pressure gauge, direct cell-center analytic values,
|
||||
kinetic energy per unit reference mass, and actual leaf cell areas. Composite
|
||||
midpoint quadrature introduces a small initial energy integration error, retained
|
||||
rather than subtracted. Mass drift is relative to downloaded t0, so density
|
||||
initialization and mixed-grid quadrature do not masquerade as time drift.
|
||||
|
||||
## Pilot scheduling and transfer-consistency scope
|
||||
|
||||
Start with one uniform case and one matched AMR case; the 16-case matrix is a
|
||||
qualification plan, not authorization to launch every case automatically. The
|
||||
GPU owner reviews the pilot before expanding it. Each process prints a flushed
|
||||
READY only after initialization, a real base step exercising every level's
|
||||
kernel/halo/transfer path, and a finite positive-density download check. That
|
||||
step belongs to the measured trajectory; it is not discarded warmup. PROGRESS
|
||||
messages contain each of the five diagnostic samples. READY alone is not an
|
||||
accuracy pass. Reports include per-level active cells, hops and collision updates.
|
||||
|
||||
The complete matrix costs 768,540,672 active collision updates and 21,504 root
|
||||
intervals, excluding ghost streaming, halos, transfers and diagnostic work.
|
||||
Uniform N256 is the largest case (268,435,456 updates / 4096 intervals per Re).
|
||||
AMR N64/depth3 needs 50,987,008 updates / 1024 intervals. These are work counts,
|
||||
not GPU wall-time estimates. Baseline comparisons remain available on failed
|
||||
screens and partial pilots; absent/nonfinite operands are null, never passing.
|
||||
|
||||
Acoustic viscosity scaling and unchanged lattice velocity do not by themselves
|
||||
prove nonequilibrium-stress consistency across refinement interfaces. A smooth
|
||||
collocated Chapman–Enskog postcollision nonequilibrium has a (tau-1)*h factor;
|
||||
the existing implementation instead uses volumetric piecewise-constant explosion
|
||||
and stream-only ghosts. Whether that actual transfer construction achieves the
|
||||
needed stress accuracy is an unresolved question for this pilot to measure.
|
||||
It is not a proven defect, and a collocated-state scaling factor must not be
|
||||
inserted into this different transfer operator without a separate derivation
|
||||
and validation. This campaign measures the current operator and makes no
|
||||
theoretical stress-consistency certification or production-kernel change.
|
||||
|
||||
## Repository integration
|
||||
|
||||
The four Python harness files remain together under tests/validation/taylor_green, so runtime provenance hashes precisely this harness directory. Formatting changes produce a new harness identity; preparation and integrated receipts must not be mixed. All analytic definitions, numerical floors, assessment gates, sample times, and source-import assertions are unchanged. Production source is unchanged from the cd3 snapshot; these periodic tests do not waive the separate channel GPU gate.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Bounded uniform-grid LES flow validation: prepared, GPU NOT RUN
|
||||
|
||||
## Status and scope
|
||||
|
||||
Prepared for independent review, then an explicitly authorized Horsea run only. No production files were edited, no normalization patch was applied to a full solver checkout, and no GPU was used. This is a small end-to-end **wall-bounded, x-periodic** shear-decay test, not a fully periodic 2D test. It exercises the actual production Simulation → Stepper → OneStep → Smagorinsky → SRT dispatch and double-buffer pull streaming, plus existing halfway bounce-back walls.
|
||||
|
||||
The original fully periodic-y plan is unsupported at the pinned revision: both `one_step_double.cu:90` and `one_step_esopull.cu:133` replace wall-adjacent incoming populations for every fluid node on y=1 and NY-2 regardless of its neighboring flags. `tests/validation/run_poiseuille.py:1–10` already documents this. Merely changing all flags to FLUID does not remove this behavior. The present test deliberately matches those real walls; it does not modify the kernels or claim a periodic-y public API.
|
||||
|
||||
Only unforced SRT, D2Q9, FP32 shifted storage and double-buffer streaming are included. No MRT/TRT, force correction, EsoPull, AMR, Jet, 3D turbulence or physical LES coefficient calibration is certified. In continuum this unidirectional incompressible shear is trace-free; small finite-Mach/discretization stresses in the LBM are not asserted to vanish exactly. The underlying scalar-helper audit remains the cleaner normalization isolation.
|
||||
|
||||
## Exact source contract
|
||||
|
||||
Baseline is explicitly repinned to the production snapshot in `cd3a657f9d3833e812c3cca173525f165b48710a`, whose reviewed AMR-channel source origin is `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. This new experiment does not inherit historical d79 GPU/flow acceptance. The original preparation remains associated with d79; its results are not rewritten.
|
||||
|
||||
`tests/validation/data/les_flow/source_pins.json` records all 101 tracked production source files from that snapshot. The runner checks every hash before importing CelerisLab or touching a GPU. It accepts only:
|
||||
|
||||
- `--variant baseline`: all baseline hashes, including helper SHA256 `ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501`.
|
||||
- `--variant corrected`: the same source bytes except helper SHA256 `a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa`, from the independently reviewed `tests/validation/les/data/smagorinsky_normalization.patch` artifact.
|
||||
|
||||
The runner records the actual checkout commit and exact imported package. It never applies a patch itself. Run it against separate isolated full checkouts. The patch candidate's minimal header tree is not a runnable solver checkout. A source mismatch is a blocker, not permission to regenerate pins from an arbitrary new checkout.
|
||||
|
||||
Relevant pinned implementation evidence:
|
||||
|
||||
- `tests/integration/test_numerical_gpu.py:24–85`: equilibrium construction, installation of spatial postcollision state, count=1 adapter, raw population observation and x-periodic/walled flags.
|
||||
- `tests/validation/run_poiseuille.py:29–78`: production Simulation use with periodic x, halfway y walls and downloaded populations.
|
||||
- `src/CelerisLab/lbm/kernels/core/layout.cuh:87–104`: actual modulo neighbors.
|
||||
- `src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh:18–38`: incoming populations copied from opposite local input directions.
|
||||
- `src/CelerisLab/lbm/kernels/operators/helpers.cuh:53–61`: LES effective omega feeds actual SRT collision.
|
||||
- `src/CelerisLab/cuda/compiler_v2.py:56–95`: compiler snapshots into private temporary build directories; package sources are not rewritten.
|
||||
|
||||
## Geometry, units and initial state
|
||||
|
||||
8×34 nodes: 8 periodic-x columns, 32 fluid rows, 2 ghost-wall rows. Relative to the bottom physical wall, fluid centers are y=0.5,...,31.5; physical halfway walls are y=0 and 32. Every x edge fluid node is explicitly FLUID; all top/bottom ghost nodes are SOLID|BC_WALL. Thus unused inlet/outlet configuration has no inlet/outlet flags to act on. There are no bodies, Jets or external forces.
|
||||
|
||||
Lattice units: dx_LBM=dt_LBM=1, rho=1, nu=0.02, tau0=0.56, U=0.04, H=32. Initial u_x(y)=U sin(pi y/H), u_y=0. The maximum Mach number is 0.0693. Height-based Re=U H/nu=64; a wavelength-based Re would differ and is not used. No dimensional physical length/time/viscosity is asserted without a user-supplied unit mapping.
|
||||
|
||||
Initialize equilibrium populations, encode shifted FP32, upload both DB buffers and set completed-count=1 following the existing integration-test adapter. That count is storage metadata: physical elapsed time starts at 0 after installation. Observe density and momentum directly from current postcollision populations, adding the descriptor weights in float64; do not mix in cached macro fields. This uses a clearly labeled validation-only raw state/flag adapter, not a promised public `set_periodic` or `set_velocity_profile` API.
|
||||
|
||||
Cases: LES enabled/closed-form with Cs=0 (molecular control), Cs=.16 (ordinary-coefficient sanity), then Cs=1 (amplified normalization diagnostic). Cs=1 is intentionally not a physically recommended SGS coefficient. Initial peak modeled nu_t/nu = Cs² Δ² U(pi/H)/nu = 0.19635; estimated effective tau remains near 0.56–0.5718, far from omega guards. Standard default Cs=.16 yields a much weaker signal at these safe parameters and is not used to make a decisive short-run normalization claim. No defaults are changed.
|
||||
|
||||
Observe at elapsed steps 519, 1038, 2075, approximately 0.1, 0.2, 0.4 molecular decay times. Three cases total 6225 measured advancing launches plus 3 warm-up launches per checkout (6228 total), three snapshots each. Population buffers contain only 272×9 FP32 entries each (about 9.6 KiB), apart from ordinary driver/compiler/context overhead. No frames, trajectories or heavy output. Full result JSON is a few tens of KiB. Runtime depends on compilation/device; no performance claim is made.
|
||||
|
||||
## Independent continuum reference
|
||||
|
||||
For u=(u(y),0), |S|=sqrt(2 S:S)=|du/dy|. Therefore
|
||||
|
||||
du/dt = d/dy [(nu + (Cs Delta)^2 |du/dy|) du/dy],
|
||||
u(0,t)=u(H,t)=0.
|
||||
|
||||
`tests/validation/les_flow_oracle.py` integrates this equation directly with centered finite-volume face fluxes and SSPRK2, at 128 and 256 cells on the same physical H=32 domain. Stability step size uses the nonlinear flux derivative nu+2(Cs Delta)^2|gradient|. Boundary face gradients use the zero Dirichlet value across a half cell. It does not import or reproduce the LBM quadratic/helper. A separate fixed-n=256 test halves the CFL factor from .4 to .2 for Cs=0,.16,1 at every checkpoint, requiring relative changes below 1e-8; measured maximum is 1.8141e-10. This isolates temporal error from the spatial-refinement test.
|
||||
|
||||
Crucially the SGS filter Delta=1 stays fixed as the CPU oracle mesh changes from dy=.25 to .125. Confusing oracle spacing with filter width would change the PDE and invalidate convergence. Future LBM grid refinement also needs an explicit physical-unit/filter-width scaling contract; this artifact does not silently reinterpret Cs or Delta when resizing H.
|
||||
|
||||
Cs=0 is checked directly against U sin(pi y/H) exp[-nu(pi/H)^2 t]. The nonlinear reference includes evolving higher harmonics, rather than assuming it remains a sine. At the final time, host oracle predicts 5.1706% SGS reduction in the fundamental amplitude and 1.5409% full-profile separation between standard and old coefficient conventions. The old convention reference is computed by changing continuum Cs to Cs/2^(1/4), purely as a diagnostic; it does not replace standard acceptance.
|
||||
|
||||
## Predeclared criteria (do not tune after seeing GPU results)
|
||||
|
||||
- Initial decoded profile relative L2 <= 1e-5; otherwise no advancing test.
|
||||
- Molecular Cs=0 profile error against analytic decay <= 0.003 at every checkpoint.
|
||||
- Cs=1 full-profile error against standard nonlinear oracle <= 0.005 at every checkpoint.
|
||||
- Oracle 128/256-grid interpolated relative difference <= 8e-5 at every checkpoint.
|
||||
- Relative error in SGS fundamental-amplitude decrement <= 0.15 at the last two checkpoints. Decrement is measured from actual Cs=0 amplitude minus actual Cs=1 amplitude; reference uses analytic Cs=0 minus the nonlinear oracle. The first checkpoint remains a transient diagnostic for this increment gate.
|
||||
- Fluid mean-density drift <= 5e-5, max |u_y| <= 1e-5 and x-column velocity spread <= 1e-6 at every checkpoint; finite populations, strictly positive finite fluid density and finite decoded velocity/profile/diagnostics required throughout observations, including startup and warm-up.
|
||||
|
||||
Reasons: molecular 0.3% leaves room for finite-lattice dispersion, equilibrium-start kinetic transient and FP32 error at H=32; nonlinear 0.5% is substantially below the predicted 1.54% old-vs-standard separation. The 15% increment tolerance is below the old convention's approximately 29% viscosity-coefficient deficit, while allowing continuum/lattice differences. Directly subtracting the two measured amplitudes reduces common lattice error. Oracle convergence is over 60 times tighter than the nonlinear profile bound. These are declared engineering test gates, not GPU-validated uncertainty estimates; a failure requires diagnosis/refinement and an explicitly revised experiment, never a post-hoc tolerance edit to obtain a pass.
|
||||
|
||||
Both baseline and candidate are judged against the same standard convention. Baseline is expected to fail SGS criteria if the flow reproduces the scalar finding; an exit 1 with valid evidence is useful. The runner reports old-convention profile error but never masks baseline failure. Candidate passing alone is insufficient if baseline does not demonstrate meaningful discrimination. Review both results and source pins together.
|
||||
|
||||
## Host verification and later GPU commands
|
||||
|
||||
From the repository root, using existing Python/NumPy and the existing pytest environment:
|
||||
|
||||
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m pytest -q tests/unit/test_les_flow_oracle.py tests/unit/test_les_flow_runner.py
|
||||
|
||||
Nine tests cover analytic decay, independent-oracle mesh convergence, symmetry/positivity/energy decay, filter invariance, fixed-grid timestep halving, signal discrimination, invalid density/velocity rejection, source pins/candidate/tamper rejection and the no-GPU-without-opt-in CLI guard. The integrated tests preserve those checks. No PyCUDA import or GPU run is required for them. Cs=.16 uses the same 0.5% full-profile safety/sanity bound; its SGS increment is not a discriminatory normalization gate.
|
||||
|
||||
The source-guard tests materialize temporary candidate headers with the existing reviewed LES preparation tool; the GPU entrypoint uses `tests/validation/run_uniform_les_flow.py`, `tests/validation/les_flow_oracle.py`, and `tests/validation/data/les_flow/source_pins.json` in that repository layout.
|
||||
|
||||
After review, make isolated full baseline and corrected checkouts of the exact base. Apply only `tests/validation/les/data/smagorinsky_normalization.patch` to the corrected checkout; do not edit the benchmark/production tree. On authorized Horsea, with existing NumPy, PyCUDA, nvcc and driver, run sequentially and select the authorized physical GPU explicitly:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --run-gpu --device 0 --root <isolated-baseline> --variant baseline --output <new-evidence-dir>/baseline.json
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --run-gpu --device 0 --root <isolated-corrected> --variant corrected --output <new-evidence-dir>/corrected.json
|
||||
|
||||
`--device 0` is the logical ordinal within CUDA_VISIBLE_DEVICES. Do not use a shell `&&` between these: baseline's expected standard-criterion failure must not suppress the corrected run. Existing output files are refused. A JSON `READY` line is printed separately for each case only after source verification, compilation, initialization, raw-state installation, one actual advancing OneStep launch, post-warm-up macro validation, complete restoration of both DB buffers/flags/count to the initial state, profile readback and CUDA synchronization. It gives source identity, device, Cs, warm-up/reset confirmation, installed-profile error and maximum measured steps. This is the first trustworthy startup checkpoint, not a statement that the physics passed. `DONE` is printed after all three cases and all criteria; failures/compile errors before that remain incomplete runs.
|
||||
|
||||
Deliver both JSON files and stdout/stderr with artifact hashes. Review before any production integration. AMR LES guards remain untouched.
|
||||
|
||||
## Isolated candidate preparation
|
||||
|
||||
Use the current validation commit for two separate full checkouts, preserving the baseline. In the corrected checkout only, apply `tests/validation/les/data/smagorinsky_normalization.patch` with `git apply`; do not commit that experimental production change or update the pins. The runner requires exactly the baseline bytes plus the reviewed helper hash. The header-only scalar diagnostic tree is insufficient for this end-to-end flow. Baseline and corrected commands must run as separate Python processes; package import caching must never combine their implementations. Both reject an imported CelerisLab package outside the explicitly supplied checkout, including an installed DynamisLab copy. Retain the actual checkout commit, variant, exact header hash, and experimental source label with each result.
|
||||
@@ -1,155 +0,0 @@
|
||||
# Single-cylinder free transverse VIV, Re 150 (Kolahdouz/Qin) — PREDECLARATION (agreed at 1abfe954)
|
||||
|
||||
(File renamed from `leontini_viv_predeclare.md`; the Leontini Re 200 case is only the queued second case below.)
|
||||
|
||||
Stage goal (Yanqi via TRON): prescribed Dütsch case + one VIV case both
|
||||
"reasonably close" to a predeclared reference = development-stage success, then
|
||||
cleanup. The frozen Dütsch settling failure (0/28 pairs at 31.25 T) stays on
|
||||
record. Practical closeness is not a formal validation claim. Agreed by
|
||||
Claude, Grok and Astra at 1abfe954; implemented in research/viv_re150/.
|
||||
|
||||
## Primary case (one case)
|
||||
|
||||
- Reference: Qin et al., arXiv:2003.12186, §4.5 (maximum amplitudes at
|
||||
U_red = 3…8: .075, .551, .520, .459, .370, .084 D in the text, as quoted by
|
||||
Grok/Astra) for the target; setup from Kolahdouz et al., arXiv:2003.12046,
|
||||
§4.1.1 (read 2026-10-30 via ar5iv): U* sweep 3–8 with amplitudes only in
|
||||
Fig. 3(b) (no text values), domain [−30, 45] D × [−30, 30] D with the
|
||||
cylinder "initially at rest and centered at the origin", uniform inflow on
|
||||
the left, zero normal traction and tangential velocity at the outlet, zero
|
||||
normal velocity and tangential traction on y = ±30 D; no initial
|
||||
perturbation and no total simulated time stated.
|
||||
- Re = 150, U* = U/(f_n D) = 5, m/(rho D²) = 2 (m* = rho_s/rho_f = 8/pi),
|
||||
zeta = 0, f_n = sqrt(K/M)/(2 pi) in vacuum. Transverse DOF only.
|
||||
- Target: A_max = .520 D (peak amplitude, Qin §4.5). Multi-code reference
|
||||
(Kolahdouz ILE-NS, Qin II-LBM). Reference uncertainty: not stated; the
|
||||
spread between codes is not quantified here.
|
||||
|
||||
## Our reproduction (declared differences)
|
||||
|
||||
- D = 40, MRT, dense double buffer; U_lat = .04, nu = U D / 150 = .010667,
|
||||
tau = .532.
|
||||
- Nominal 75 D × 60 D lattice allocation (3000 × 2400), based on the
|
||||
Kolahdouz §4.1.1 layout. Coordinate clarification of the existing run:
|
||||
the driver uses `(30 D, (ny - 1)/2)` = `(1200, 1199.5)` in lattice
|
||||
coordinates, or `(30 D, 29.9875 D)`. The earlier `(30 D, 30 D)` wording
|
||||
described the nominal box, not the exact executed centre.
|
||||
Free-slip reflection planes are at `y = .5` and `y = ny - 1.5 = 2398.5`;
|
||||
their separation is 2398 cells = 59.95 D, with the centre exactly midway,
|
||||
29.975 D from each wall. Node counts are not physical wall-to-wall lengths.
|
||||
This corrects the description, without changing the historical geometry
|
||||
or results. Qin's own box ([0, 40 D] × [0, 20 D], centre (10 D, 10 D)) is
|
||||
the target paper's, not ours.
|
||||
- Boundaries: uniform Zou–He inlet, NEQ outlet (rho = 1, backflow clamp),
|
||||
free-slip y walls. Kolahdouz uses a traction outlet: declared mismatch.
|
||||
- Qin's own VIV box (40 D × 20 D) is not used; a smaller box would be a
|
||||
declared cost cut with more blockage, not "the same case".
|
||||
- Allowed y offset ±1.5 D; peak transverse speed ≈ .026 cells/step (bound .5).
|
||||
|
||||
## Structure, coupling and time levels
|
||||
|
||||
- M = 2 rho D² (lattice rho = 1), f_n = U/(5 D) = 2e-4 per step, K = M (2 pi f_n)²,
|
||||
zeta = 0. Equation of motion uses the structural mass only.
|
||||
- Per-step sequence (binding): known (y_n, v_n) and the last completed force
|
||||
F_last → candidate (y_{n+1}, v_{n+1}) by the update below → `set_body` with
|
||||
offset y_{n+1} and wall velocity (y_{n+1} − y_n)/dt with lattice dt = 1 (interval average; a
|
||||
different quantity from the endpoint v_{n+1}) → `run(1)` → only on success:
|
||||
commit y_n ← y_{n+1}, v_n ← v_{n+1}, F_last ← force read for that step (failure
|
||||
semantics: see the window section). Whether the
|
||||
measured force represents the interval's start, midpoint or end is not
|
||||
asserted; the one-step lag is declared, not corrected.
|
||||
- Initial state: fluid initialized by `Simulation.initialize()` to the uniform
|
||||
inlet velocity U everywhere (the code's actual initial condition; the
|
||||
source gives no fluid initial condition), body at rest at the declared
|
||||
centre, y_0 = v_0 = 0, F_last = 0.
|
||||
- Structural update over one step with constant force F = F_last (exact
|
||||
harmonic solution, zeta = 0): y_e = F/K, w = 2 pi f_n,
|
||||
y^{n+1} = y_e + (y^n − y_e) cos w + (v^n / w) sin w,
|
||||
v^{n+1} = −(y^n − y_e) w sin w + v^n cos w.
|
||||
- Primary RHS (single coupled run): CME + Wen (theory-motivated stage choice:
|
||||
Galilean-invariant link correction; G4 could not separate it from CME).
|
||||
Raw CME and the node terms are logged as diagnostics of that trajectory;
|
||||
the node impulse is not added. A run with CME-only as RHS is a separate,
|
||||
labelled sensitivity case, decided separately. Neither RHS is claimed proven.
|
||||
|
||||
## Window, failure semantics, acceptance (engineering, development stage)
|
||||
|
||||
Clocks: lattice dt = 1; T_n = 1/f_n = 5,000 steps. Response cycles are
|
||||
counted from the measured signal; T_n is used only for the seed check.
|
||||
|
||||
- Start: body at rest at the declared centre (the only initial condition the
|
||||
source states), y_0 = v_0 = 0, F_last = 0; fluid uniform at U (as
|
||||
initialized by the code; our choice, not the source's).
|
||||
- Seed (conditional, one time, declared): at step n_seed = 200,000
|
||||
(t = 40 T_n), if max |y_t| over states t in [175,000, 200,000) is below
|
||||
.01 D, then
|
||||
in that step, after the check and before the candidate update, set
|
||||
v_n += 2 pi f_n (.01 D) (F_last unchanged) and proceed normally; the
|
||||
geometry moves continuously through the update. `seed_applied`, step and
|
||||
size are logged; never repeated or changed.
|
||||
- Cycle boundary (discrete, fixed threshold): step n closes a cycle iff
|
||||
y[n−1] < 0 and y[n] ≥ 0 (upward crossing of the equilibrium y = 0);
|
||||
y_0 = 0 is not a crossing; no boundary fewer than 2 samples after the
|
||||
previous one.
|
||||
- Per cycle: positive peak max(y), negative peak min(y), mean offset
|
||||
(time-mean of y), amplitude a = (max − min)/2 (the only amplitude used for
|
||||
settling).
|
||||
- Eligibility: a completed cycle is counted (toward the settling proof and
|
||||
the 150 budget) only if a ≥ .01 D; ineligible cycles are logged, never
|
||||
counted, so near-zero motion cannot settle or pass.
|
||||
- Seed reset: if the seed is applied, the incomplete cycle at n_seed is
|
||||
discarded and the settling proof starts empty; the 150 budget and the
|
||||
native cap keep running from t = 0.
|
||||
- Windows: adjacent non-overlapping blocks of 10 counted cycles;
|
||||
W_k = mean amplitude of block k (W_k ≥ .01 D by eligibility, so no zero
|
||||
denominator).
|
||||
- Block rule R(k), k ≥ 2: |W_k − W_{k−1}| / W_{k−1} ≤ .02 and, for the last 5
|
||||
cycles of block k, max_i |a_i − ā_5| / ā_5 ≤ .01 with ā_5 their mean.
|
||||
- Settled at the end of block k when R(k) holds. Scoring = the next two
|
||||
blocks k+1, k+2 (20 counted cycles); both must satisfy R. If one fails,
|
||||
that block becomes the new reference: the settling proof restarts from it
|
||||
and continues within the caps (deterministic; no other retry). A pass
|
||||
requires one complete stable 20-cycle scoring window before a cap; success
|
||||
stops after its 20th cycle.
|
||||
- Caps (both always armed; the first hit wins and is reported): 150 counted
|
||||
response cycles including the 20 scoring cycles (so settling must complete
|
||||
by counted cycle 130) → `timeout_response_cycles`; 1,000,000 native steps
|
||||
(200 T_n) → `timeout_native_steps`.
|
||||
- Durations reported: total steps, t U/D, counted and uncounted cycles,
|
||||
measured T_resp (mean boundary interval); N_resp / f_n only as a labelled
|
||||
secondary clock.
|
||||
- Growth is not a failure: initial VIV growth is physical. Checked every
|
||||
step (FAIL-CLOSED): non-finite y, v or force; |y| > 1.0 D (the tighter
|
||||
gate; about twice the largest reference peak, .551 D) or the ±1.5 D offset
|
||||
box reached; no_donor > 0; a failed upload/step/sync.
|
||||
- On any failure the driver stops and reports; it never retries,
|
||||
sub-iterates, retunes the RHS or parameters, or starts a second case.
|
||||
Device-side failures (upload/step/sync, no_donor) mark the Simulation
|
||||
failed through the existing lifecycle; input rejection by `set_body`
|
||||
(offset box, speed, step bound) and timeouts end the driver run without
|
||||
that mark.
|
||||
- Target metric over the 20 scoring cycles: A_max/D = max over those cycles
|
||||
of max(|positive peak|, |negative peak|) about y = 0 (Qin reports "maximum
|
||||
amplitude"); also reported: mean amplitude, positive and negative peaks,
|
||||
mean offset, f/f_n (no target).
|
||||
- Acceptance: |A_max − .520 D| ≤ .052 D (±10 %), stated together with the
|
||||
boundary mismatch and the absence of a quoted reference uncertainty. A pass
|
||||
is "reasonably close" for the stage: not grid convergence, not force-RHS
|
||||
validation, and it does not touch the Dütsch settling record.
|
||||
- Driver reads force and the small diagnostics per step only; field reads
|
||||
only at a few predeclared snapshots. No runtime claim: there is no measured
|
||||
timing on this grid yet (the D40/60D receipt is a different, sampled case).
|
||||
|
||||
## Plumbing smoke (not the benchmark)
|
||||
|
||||
`run.py --smoke --D 20 --steps 32` runs exactly 32 steps of the same BC
|
||||
package and coupling loop (no seed, settle or score; never scored);
|
||||
status `smoke_ok` iff every step is finite, no_donor stays 0 and the body
|
||||
stays inside its box. The physical case is only the default invocation
|
||||
(`--D 40`, no `--smoke`).
|
||||
|
||||
## Queued, not in this stage
|
||||
|
||||
Leontini et al. JFS 22 (2006) Re = 200, m* = 10, zeta = 0.01, U* = 5.0 (needs
|
||||
the owners' c_crit convention and a Fig. 1 digitization). D80, literature
|
||||
claims, pinball, AMR, BC redesign, threshold changes.
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Public API demo: fixed 2:1 patch with a stationary, optionally actuated circle.
|
||||
|
||||
From this checkout, after checking the selected physical GPU is idle:
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py
|
||||
... --speed -0.002 --mode slot_reservoir
|
||||
Both levels use blocked storage (--base-block, --fine-block tiles).
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py --fine-block 16 20
|
||||
... --speed -0.002 --fine-block 16 20 --mode slot_reservoir
|
||||
Omit --fine-block for the (equivalent) dense fine storage.
|
||||
|
||||
A short smoke run (default 128 coarse steps): it shows setup, stepping and
|
||||
fluid-only diagnostics, not a developed flow, accuracy or long-time stability.
|
||||
@@ -40,13 +40,13 @@ def config(mode):
|
||||
def leaf_masks(run, mode):
|
||||
"""Known fluid leaves: uncovered coarse cells (minus wall rows in slot mode)
|
||||
and fine ACTIVE cells outside the body."""
|
||||
p = run.hierarchy.pairs[0]
|
||||
p = run.patch
|
||||
x0, y0, x1, y1 = p.bounds
|
||||
coarse = np.ones(p.coarse_shape, bool)
|
||||
coarse[y0:y1, x0:x1] = False
|
||||
if mode == "slot_reservoir":
|
||||
coarse[[0, -1]] = False
|
||||
solid = (run.boundaries[1].flags.reshape(p.fine_shape) & SOLID) != 0
|
||||
solid = (run.fine_boundary.flags.reshape(p.fine_shape) & SOLID) != 0
|
||||
return coarse, (p.fine_kind == ACTIVE) & ~solid
|
||||
|
||||
|
||||
@@ -71,10 +71,8 @@ def main():
|
||||
parser.add_argument("--mode", choices=("periodic", "slot_reservoir"), default="periodic")
|
||||
parser.add_argument("--speed", type=float, default=.002,
|
||||
help="normal surface velocity, coarse lattice units (+ blows, - sucks, 0 passive)")
|
||||
parser.add_argument("--base-block", type=int, nargs=2, metavar=("BY", "BX"), default=(12, 8),
|
||||
help="root tile; must divide the 24 x 32 root")
|
||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"), default=(16, 20),
|
||||
help="fine tile; must divide the whole fine allocation (32 x 40 here)")
|
||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"),
|
||||
help="blocked fine storage; must divide the whole fine allocation (32 x 40 here)")
|
||||
parser.add_argument("--steps", type=int, default=128)
|
||||
args = parser.parse_args()
|
||||
if args.steps < 0:
|
||||
@@ -88,20 +86,20 @@ def main():
|
||||
body = SimObject(0, geometry=CircleGeometry(*CENTER, RADIUS), center=CENTER, radius=RADIUS,
|
||||
surface_actuation=spec)
|
||||
cfg = config(args.mode)
|
||||
fine_block = tuple(args.fine_block) if args.fine_block else None
|
||||
# The caller owns the CUDA context; the stepper must be closed first (the
|
||||
# nested with-statement does that).
|
||||
with CudaContext(0) as context, FixedPatchStepper(
|
||||
cfg, context=context, coarse_boundary=args.mode, refinements=[BOUNDS],
|
||||
level_blocks=[tuple(args.base_block), tuple(args.fine_block)],
|
||||
level_objects=[(), (body,)]) as run:
|
||||
p = run.hierarchy.pairs[0]
|
||||
cfg, BOUNDS, context=context, coarse_boundary=args.mode,
|
||||
fine_objects=[body], fine_block=fine_block) as run:
|
||||
p = run.patch
|
||||
# Rest equilibrium is already postcollision G, so no initial collision is
|
||||
# needed. Non-leaf entries (covered, ghost, solid) are discarded by initialize.
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
run.initialize(np.broadcast_to(rest, (9, *p.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *p.fine_shape)))
|
||||
storage = f"blocked root {tuple(args.base_block)} fine {tuple(args.fine_block)}"
|
||||
print(f"mode={args.mode} storage={storage} speed={args.speed:+g} "
|
||||
storage = f"blocked {fine_block}" if fine_block else "dense"
|
||||
print(f"mode={args.mode} fine storage={storage} speed={args.speed:+g} "
|
||||
f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})")
|
||||
masks = leaf_masks(run, args.mode)
|
||||
start = leaf_diagnostics(*run.download(), masks)
|
||||
|
||||
@@ -31,7 +31,6 @@ from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
|
||||
BOUNDS, AMPLITUDE, WIDTH = (16, 4, 32, 28), .02, 12
|
||||
LEVEL_BLOCKS = [(12, 8), (8, 8)] # blocked storage: 48x48 root, 56x40 fine window
|
||||
TIMES, MAX_MACH, MAX_SECONDS = (0, 300, 1200), .1, 120
|
||||
|
||||
|
||||
@@ -133,8 +132,7 @@ def main():
|
||||
lattice_model='D2Q9', collision='SRT', streaming='double_buffer',
|
||||
store_precision='FP32', ddf_shifting=False, les_enabled=False)
|
||||
report = dict(config=dict(nx=48, ny=48, bounds=BOUNDS, W=WIDTH, A=AMPLITUDE, nu=.1,
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir',
|
||||
level_blocks=LEVEL_BLOCKS),
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir'),
|
||||
guards=dict(check_every=50, positive_density=True, finite=True,
|
||||
max_Mach=MAX_MACH, max_seconds=MAX_SECONDS,
|
||||
timing='cooperative checks, not an exact interruption deadline'),
|
||||
@@ -150,10 +148,9 @@ def main():
|
||||
samples=[], status='running', last_synchronized_coarse_step=0)
|
||||
start, run = time.monotonic(), None
|
||||
try:
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context,
|
||||
coarse_boundary='slot_reservoir', refinements=[BOUNDS],
|
||||
level_blocks=LEVEL_BLOCKS) as run:
|
||||
p = run.hierarchy.pairs[0]
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, BOUNDS, context=context,
|
||||
coarse_boundary='slot_reservoir') as run:
|
||||
p = run.patch
|
||||
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
|
||||
masks[0][[0, -1]] = False
|
||||
masks[0][4:28, 16:32] = False
|
||||
|
||||
@@ -16,7 +16,7 @@ from CelerisLab.config import load_lbm_config
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.equilibrium import compute_feq_d2q9, compute_macro_from_ddf
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
|
||||
|
||||
|
||||
def shear_equilibrium(shape, origin, dx, period):
|
||||
@@ -48,9 +48,8 @@ def main():
|
||||
nx=16, ny=16, viscosity=.1, velocity=1e-3, rho=1.0,
|
||||
lattice_model="D2Q9", collision="SRT", streaming="double_buffer",
|
||||
store_precision="FP32", ddf_shifting=False, les_enabled=False)
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
|
||||
level_blocks=[(16, 16), (24, 24)]) as run:
|
||||
p = run.hierarchy.pairs[0]
|
||||
with CudaContext(0) as context, PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as run:
|
||||
p = run.patch
|
||||
coarse = shear_equilibrium(p.coarse_shape, (0, 0), 1.0, cfg.ny)
|
||||
fine = shear_equilibrium(p.fine_shape, p.fine_origin, .5, cfg.ny)
|
||||
coarse_leaf = np.ones(p.coarse_shape, dtype=bool)
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
"""Sparse scientific frames and the approved new-reproduce presentation idiom.
|
||||
|
||||
Frame gears/UI colors/rotation sectors derive from DynamisLab continuous_video.py
|
||||
(read-only SHA in docs). Modern velocity decoding and moving coordinates are used.
|
||||
All camera limits, vorticity limits, and motion scale are fixed, never following y.
|
||||
"""
|
||||
from functools import lru_cache
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
D,U,CY=40.,.005,510.5
|
||||
SIZE=(1280,800)
|
||||
CW,CCW='#8854B8','#138F78'
|
||||
MAIN=(0,120,1280,440)
|
||||
INSET=(960,492,1240,772)
|
||||
MAIN_LIMITS=(6.4,57.6,-6.4,6.4)
|
||||
INSET_LIMITS=(28.75,33.75,-2.5,2.5)
|
||||
|
||||
|
||||
def frame_schedule(steps,base_interval,accelerated=False):
|
||||
"""Original 15-frame gear holds; only readback cadence changes."""
|
||||
if accelerated:
|
||||
ramp=15*(1+2+5)*base_interval
|
||||
if steps<=2*ramp:
|
||||
raise ValueError('too short for both display ramps')
|
||||
blocks=[(s,15*s*base_interval) for s in (1,2,5)]
|
||||
blocks += [(10,steps-2*ramp)]
|
||||
blocks += [(s,15*s*base_interval) for s in (5,2,1)]
|
||||
else:
|
||||
blocks=[(1,steps)]
|
||||
result,done=[],0
|
||||
for speed,duration in blocks:
|
||||
end=done+duration
|
||||
while done<end:
|
||||
done=min(done+speed*base_interval,end)
|
||||
result.append((done,speed))
|
||||
return result
|
||||
|
||||
|
||||
def video_schedule(phase_steps,smoke=False):
|
||||
if smoke:
|
||||
phase=frame_schedule(phase_steps,8)
|
||||
else:
|
||||
# First 80 D/U of each phase is fast display; last 80 is normal.
|
||||
half=phase_steps//2
|
||||
phase=frame_schedule(half,800,True)
|
||||
phase += [(half+n,s) for n,s in frame_schedule(phase_steps-half,800)]
|
||||
return {p*phase_steps+n:s for p in range(3) for n,s in phase}
|
||||
|
||||
|
||||
def vorticity(ux,uy,flags):
|
||||
"""Centered dx=1 curl; mask every stencil touching nonfluid, no filling."""
|
||||
ux,uy=np.asarray(ux,dtype=float),np.asarray(uy,dtype=float)
|
||||
fluid=(flags&1)!=0
|
||||
valid=(fluid[1:-1,1:-1]&fluid[1:-1,:-2]&fluid[1:-1,2:]&
|
||||
fluid[:-2,1:-1]&fluid[2:,1:-1])
|
||||
curl=((uy[1:-1,2:]-uy[1:-1,:-2])-(ux[2:,1:-1]-ux[:-2,1:-1]))/2
|
||||
result=np.full(ux.shape,np.nan)
|
||||
result[1:-1,1:-1]=np.where(valid,curl*D/U,np.nan)
|
||||
return result
|
||||
|
||||
|
||||
def pixel(x,y,box,limits):
|
||||
left,top,right,bottom=box
|
||||
x0,x1,y0,y1=limits
|
||||
return left+(x-x0)/(x1-x0)*(right-left),bottom-(y-y0)/(y1-y0)*(bottom-top)
|
||||
|
||||
|
||||
@lru_cache(maxsize=12)
|
||||
def font(size,bold=False):
|
||||
from PIL import ImageFont
|
||||
name='DejaVuSans-Bold.ttf' if bold else 'DejaVuSans.ttf'
|
||||
return ImageFont.truetype('/usr/share/fonts/truetype/dejavu/'+name,size)
|
||||
|
||||
|
||||
def draw_circle(draw,x,y,r,wall_speed,box,limits):
|
||||
cx,cy=pixel(x,y,box,limits)
|
||||
radius=r*(box[2]-box[0])/(limits[1]-limits[0])
|
||||
rect=(cx-radius,cy-radius,cx+radius,cy+radius)
|
||||
draw.ellipse(rect,fill='white',outline='#263D4B',width=2)
|
||||
if wall_speed is None:
|
||||
return
|
||||
fraction=abs(wall_speed)/(12*U)
|
||||
if not np.isfinite(fraction) or fraction>1.00001:
|
||||
raise RuntimeError('applied rotation exceeds fixed display reference')
|
||||
r2=radius*.72
|
||||
inner=(cx-r2,cy-r2,cx+r2,cy+r2)
|
||||
draw.ellipse(inner,fill='#E4E9ED')
|
||||
angle=min(fraction,1)*360
|
||||
start,end=(-90,-90+angle) if wall_speed<0 else (-90-angle,-90)
|
||||
if angle>0:
|
||||
draw.pieslice(inner,start,end,fill=CW if wall_speed<0 else CCW)
|
||||
|
||||
|
||||
class Video:
|
||||
def __init__(self,out,scene,bodies,phase_steps,smoke=False,source='unknown'):
|
||||
from PIL import Image
|
||||
from matplotlib import colormaps
|
||||
self.out,self.scene,self.bodies=out,scene,bodies
|
||||
self.schedule=video_schedule(phase_steps,smoke)
|
||||
self.phase_steps=phase_steps
|
||||
self.source=source
|
||||
self.records=[]
|
||||
self.palette=np.rint(colormaps['RdBu_r'](np.linspace(0,1,21))[:,:3]*255).astype('uint8')
|
||||
self.log=open(out/'ffmpeg.log','wb')
|
||||
self.proc=subprocess.Popen(['ffmpeg','-hide_banner','-loglevel','warning','-n',
|
||||
'-f','rawvideo','-pix_fmt','rgb24','-s','1280x800','-r','30','-i','pipe:0',
|
||||
'-an','-c:v','libx264','-threads','4','-preset','fast','-crf','18',
|
||||
'-pix_fmt','yuv420p','-movflags','+faststart',str(out/'continuous_viv.mp4')],
|
||||
stdin=subprocess.PIPE,stderr=self.log)
|
||||
self.closed=False
|
||||
self.seconds=0.
|
||||
|
||||
def capture(self,sim,step,phase,eff,rows):
|
||||
speed=self.schedule.get(step)
|
||||
if speed is None:
|
||||
return
|
||||
from PIL import Image,ImageDraw
|
||||
start=time.monotonic()
|
||||
macro=sim.get_macroscopic()
|
||||
flags=sim.get_flags().reshape(sim.field.ny,sim.field.nx)
|
||||
fluid=(flags&1)!=0
|
||||
if any(not np.isfinite(macro[k][fluid]).all() for k in ('rho','ux','uy')):
|
||||
raise RuntimeError('nonfinite fluid field at video sample')
|
||||
omega=vorticity(macro['ux'],macro['uy'],flags)
|
||||
finite=np.isfinite(omega)
|
||||
index=np.floor((np.clip(np.nan_to_num(omega),-3,3)+3)/6*21).astype(int).clip(0,20)
|
||||
rgb=self.palette[index]
|
||||
rgb[~finite]=[232,235,237]
|
||||
field=Image.fromarray(rgb[::-1])
|
||||
canvas=Image.new('RGB',SIZE,'white')
|
||||
draw=ImageDraw.Draw(canvas)
|
||||
for box,limits in ((MAIN,MAIN_LIMITS),(INSET,INSET_LIMITS)):
|
||||
x0,x1,y0,y1=limits
|
||||
# Lattice cell-centre coordinates -> image pixel-edge coordinates.
|
||||
crop=(x0*D+.5,sim.field.ny-(CY+y1*D+.5),
|
||||
x1*D+.5,sim.field.ny-(CY+y0*D+.5))
|
||||
picture=field.crop(crop).resize((box[2]-box[0],box[3]-box[1]),Image.Resampling.BOX)
|
||||
canvas.paste(picture,box[:2])
|
||||
if box==INSET:
|
||||
_,eq=pixel(30,0,box,limits)
|
||||
for x in range(box[0],box[2],10):
|
||||
draw.line((x,eq,min(x+5,box[2]),eq),fill='#657887',width=1)
|
||||
if self.scene=='karman':
|
||||
draw_circle(draw,10,0,1,None,box,limits) if box==MAIN else None
|
||||
for bid,w in zip(self.bodies,eff):
|
||||
obj=sim.bodies.objects[bid]
|
||||
x,y=obj.motion.centre(obj.motion.applied)
|
||||
draw_circle(draw,x/D,(y-CY)/D,obj.radius/D,float(w),box,limits)
|
||||
x0,y0=pixel(INSET_LIMITS[0],INSET_LIMITS[2],MAIN,MAIN_LIMITS)
|
||||
x1,y1=pixel(INSET_LIMITS[1],INSET_LIMITS[3],MAIN,MAIN_LIMITS)
|
||||
draw.rectangle((x0,y1,x1,y0),outline='#647C8C',width=1)
|
||||
draw.rectangle(INSET,outline='#647C8C',width=1)
|
||||
def text(x,y,value,size=16,color='#263D4B',bold=False):
|
||||
draw.text((x,y),value,font=font(size,bold),fill=color)
|
||||
on=phase=='on'
|
||||
text(28,15,'INCOMING FLOW',12,'#667887',True)
|
||||
text(28,39,'Steady inflow' if self.scene=='steady' else 'Periodic cylinder wake',29,bold=True)
|
||||
text(28,83,'One continuous flow and oscillator trajectory | Re_D = 50 | zero structural damping',15)
|
||||
text(850,18,'CYLINDER ROTATION',12,bold=True)
|
||||
text(850,43,'Clockwise',14,CW);text(1020,43,'Counterclockwise',14,CCW)
|
||||
text(850,70,'Sector angle = actual surface speed',13,'#667887')
|
||||
text(850,90,'Full circle = 12 x reference inflow speed',13,'#667887')
|
||||
draw.line((24,453,1256,453),fill='#E5EBEF')
|
||||
text(28,465,'CLOAKING CONTROL',12,'#667887',True)
|
||||
draw.rounded_rectangle((28,490,92,524),radius=17,fill='#137F73' if on else '#A8B4BC')
|
||||
cx=75 if on else 45
|
||||
draw.ellipse((cx-12,495,cx+12,519),fill='white')
|
||||
text(108,491,'ON' if on else 'OFF',22,'#137F73' if on else '#667887',True)
|
||||
text(195,493,('Fixed rotations' if self.scene=='steady' else 'Frozen PPO') if on else 'Zero rotation',18)
|
||||
text(570,467,f't U/D = {step*U/D:.2f}',18,bold=True)
|
||||
local=(step-1)%self.phase_steps+1
|
||||
transition=(phase!='off_before' and local*U/D<=1.)
|
||||
text(570,495,f'Playback {speed}x | '+('switch transient' if transition else phase.replace('_',' ')),16)
|
||||
text(960,467,f'Fixed lab-frame inset | y/D={rows[-1,1]/D:+.3f}',13,bold=True)
|
||||
# Motion trace: fixed +/-D limits, a fixed 60 D/U trailing time span.
|
||||
box=(55,560,895,732)
|
||||
draw.rectangle(box,outline='#CCD6DD')
|
||||
middle=(box[1]+box[3])/2
|
||||
draw.line((box[0],middle,box[2],middle),fill='#A8B4BC')
|
||||
t=step*U/D
|
||||
left=max(0.,t-60)
|
||||
first=max(0,int(left*D/U))
|
||||
sample=rows[first::max(1,(len(rows)-first)//1200)]
|
||||
xy=[(box[0]+(r[0]*U/D-left)/60*(box[2]-box[0]),middle-r[1]/D*(box[3]-box[1])/2) for r in sample]
|
||||
if len(xy)>1:draw.line(xy,fill='#263D4B',width=2)
|
||||
text(28,535,'Transverse displacement y/D (fixed scale; dashed inset line = equilibrium)',14)
|
||||
text(24,554,'+1',12);text(25,642,'0',12);text(24,722,'-1',12)
|
||||
text(55,739,f'{left:.1f}',12);text(835,739,f'{left+60:.1f}',12)
|
||||
text(350,739,'response time t U/D',12)
|
||||
text(28,774,f'Vorticity D/U: fixed [-3, +3] | exploratory, not convergence proof | source {self.source[:8]}',12)
|
||||
self.proc.stdin.write(canvas.tobytes())
|
||||
local=step%self.phase_steps
|
||||
if not self.records or local in (0,self.phase_steps//2) or self.records[-1]['phase']!=phase:
|
||||
canvas.save(self.out/f'keyframe-{step:09d}.png')
|
||||
elapsed=time.monotonic()-start
|
||||
self.seconds+=elapsed
|
||||
self.records.append(dict(frame=len(self.records),step=step,absolute_clock=sim.stepper.step_count,
|
||||
tU_D=t,phase=phase,playback_speed=speed,y_D=float(rows[-1,1]/D),
|
||||
surface_speeds=eff.tolist(),render_s=elapsed,
|
||||
fraction_finite_outside_color_limits=float(np.mean(np.abs(omega[finite])>3))))
|
||||
|
||||
def close(self):
|
||||
if self.closed:return
|
||||
self.closed=True
|
||||
self.proc.stdin.close()
|
||||
code=self.proc.wait(timeout=60)
|
||||
self.log.close()
|
||||
receipt=dict(frames=self.records,frame_count=len(self.records),fps=30,size=SIZE,
|
||||
duration_s=len(self.records)/30,render_total_s=self.seconds,ffmpeg_exit=code,
|
||||
normalization='centered dx=1 vorticity D/U, all 5 stencil cells fluid',
|
||||
color_limits=[-3,3],color_bins=21,main_limits_D=MAIN_LIMITS,inset_limits_D=INSET_LIMITS,
|
||||
y_origin_lattice=CY,trace_limits_D=[-1,1],inset_camera='fixed lab frame',
|
||||
acceleration='frame cadence only; native solver and SI unchanged')
|
||||
(self.out/'video.json').write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
if code:raise RuntimeError(f'ffmpeg exit {code}')
|
||||
@@ -1,222 +0,0 @@
|
||||
"""Continuous OFF -> ON -> OFF VIV illustration; docs/continuous_viv_video.md.
|
||||
|
||||
Each invocation owns ONE Simulation and ONE oscillator. Rendering never advances
|
||||
the solver or changes the policy clock. No topology changes or moving restart.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from research.karman_viv import run as base
|
||||
from research.continuous_viv.render import Video, frame_schedule
|
||||
|
||||
D, U, SCALE, SI = 40., .005, 2., 3200
|
||||
PHASES = ('off_before', 'on', 'off_after')
|
||||
STEADY_WALL = np.array([0., -5.1, 5.1]) * U
|
||||
|
||||
|
||||
def timing(scene, smoke):
|
||||
if smoke:
|
||||
return 1,32,32
|
||||
# Prior video's steady interval was 1000 at D20; diffusive D40 uses 4000.
|
||||
return (320,4000,2048000) if scene=='steady' else (400,SI,2048000)
|
||||
|
||||
|
||||
def command(scene, phase, obs, model):
|
||||
"""Physical surface speeds; OFF is not a zero normalized PPO action."""
|
||||
if phase != 'on':
|
||||
return np.zeros(3), np.array([0., .5, -.5])
|
||||
if scene == 'steady':
|
||||
return STEADY_WALL.copy(), np.full(3, np.nan)
|
||||
action = np.asarray(model.predict(obs, deterministic=True)[0], np.float32)
|
||||
if action.shape != (3,) or not np.isfinite(action).all() or (np.abs(action)>1).any():
|
||||
raise RuntimeError('invalid PPO action')
|
||||
return base.wall_target(action, SCALE), action
|
||||
|
||||
|
||||
def add_scene(sim, scene):
|
||||
if scene == 'karman':
|
||||
return base.add_scene(sim, SCALE, moving=True)
|
||||
from CelerisLab.body.motion import Translation
|
||||
probes = [sim.add_body('sensor', c, 10.) for c in base.scaled_points(base.PROBES, SCALE)]
|
||||
envelope = Translation((0., 0., -D, D), .025, shared=True)
|
||||
bodies = [sim.add_body('circle', c, D/2, translation=envelope)
|
||||
for c in base.scaled_points(base.CENTRES, SCALE)]
|
||||
return probes, bodies
|
||||
|
||||
|
||||
def evolve(sim, probes, bodies, structure, force, *, scene, model, norm,
|
||||
intervals, si, out, render, start, wall_cap):
|
||||
"""One native-step loop across all phases; completed SI feeds the next SI."""
|
||||
from CelerisLab.body.motion import MovingTables
|
||||
cols = {k:i for i,k in enumerate(MovingTables.DIAG)}
|
||||
count = len(PHASES)*intervals*si
|
||||
rows = np.lib.format.open_memmap(out/'steps.npy', mode='w+', dtype=np.float64,
|
||||
shape=(count, len(base.STEP_COLUMNS)))
|
||||
# The completed count is authoritative after any interrupted run.
|
||||
completed = 0
|
||||
obs = np.zeros(12, np.float32)
|
||||
signals, switches = [], []
|
||||
origin_clock = sim.stepper.step_count
|
||||
status = 'running'
|
||||
try:
|
||||
for phase in PHASES:
|
||||
switches.append(dict(phase=phase, step=completed, absolute_clock=sim.stepper.step_count,
|
||||
y=structure.y, v=structure.v, previous_rhs=force,
|
||||
input_observation=obs.tolist()))
|
||||
for i in range(intervals):
|
||||
target, action = command(scene, phase, obs, model)
|
||||
# Match the frozen inference contract: reset the EMA every SI.
|
||||
eff = np.zeros(3)
|
||||
sensor_sum, force_sum = np.zeros(6), np.zeros(6)
|
||||
for k in range(si):
|
||||
if k % 1000 == 0:
|
||||
base.check_time(start, wall_cap)
|
||||
y1, v1 = structure.candidate(force)
|
||||
if not np.isfinite([y1,v1]).all() or abs(y1)>D:
|
||||
raise RuntimeError('nonfinite structure or |y| > D; no retuning')
|
||||
eff = base.ema(eff, target, SCALE)
|
||||
omega = eff/(D/2)
|
||||
for bid,w in zip(bodies,omega):
|
||||
sim.set_body(bid, omega=float(w), offset=(0.,y1), vy=y1-structure.y)
|
||||
sim.run(1, zero_obs=True, sync_obs=True)
|
||||
d = sim.moving_diagnostics()
|
||||
cme,wen = d[:,cols['cme_y']], d[:,cols['wen_y']]
|
||||
node = d[:,cols['cover_jy']]-d[:,cols['uncover_jy']]
|
||||
row = (completed+1,y1,v1,*cme,*wen,*node,
|
||||
d[:,cols['eq_only']].sum(),d[:,cols['no_donor']].sum(),
|
||||
d[:,cols['crossings']].sum(),*omega)
|
||||
if not np.isfinite(row).all() or row[13] != 0:
|
||||
raise RuntimeError('nonfinite moving diagnostic or no_donor')
|
||||
f=np.array([v for p in bodies for v in sim.read_force(p)[:2]])
|
||||
s=np.array([v for p in probes for v in sim.read_sensor(p)[:2]])
|
||||
if not np.isfinite([f,s]).all():
|
||||
raise RuntimeError('nonfinite force/sensor observation')
|
||||
sensor_sum += s
|
||||
force_sum += f
|
||||
force=float(cme.sum(dtype=np.float64)+wen.sum(dtype=np.float64))
|
||||
rows[completed]=row
|
||||
completed += 1
|
||||
structure.y,structure.v=y1,v1
|
||||
if sim.stepper.step_count != origin_clock+completed:
|
||||
raise RuntimeError('noncontinuous solver clock')
|
||||
render(sim, completed, phase, eff, rows[:completed])
|
||||
if scene == 'karman':
|
||||
raw, obs, clipped = base.observation(sensor_sum/si,force_sum/si,SCALE,norm)
|
||||
else:
|
||||
# Steady uses prescribed rotations, not a model or borrowed norm.
|
||||
raw=np.r_[sensor_sum/si,force_sum/si]
|
||||
clipped=0
|
||||
signals.append(dict(phase=phase, step=completed, raw=raw.tolist(),
|
||||
observation=obs.tolist(), clipped=clipped,
|
||||
action=action.tolist() if np.isfinite(action).all() else None,
|
||||
target_surface_speed=target.tolist()))
|
||||
if (i+1)%10==0 or i+1==intervals:
|
||||
rows.flush()
|
||||
progress=dict(phase=phase, interval=i+1, completed_steps=completed,
|
||||
tU_D=completed*U/D, y_D=structure.y/D,
|
||||
elapsed_s=time.monotonic()-start)
|
||||
(out/'progress.json').write_text(json.dumps(progress)+'\n')
|
||||
print(json.dumps(progress),flush=True)
|
||||
status='duration_completed_not_physics_qualified'
|
||||
except BaseException as exc:
|
||||
status=f'error: {type(exc).__name__}: {exc}'
|
||||
raise
|
||||
finally:
|
||||
rows.flush()
|
||||
(out/'signals.json').write_text(json.dumps(signals,allow_nan=False)+'\n')
|
||||
report=dict(status=status,completed_steps=completed,origin_clock=origin_clock,
|
||||
final_clock=sim.stepper.step_count,switches=switches,
|
||||
elapsed_s=time.monotonic()-start,columns=base.STEP_COLUMNS,
|
||||
M=structure.mass,K=structure.stiffness,fn=structure.frequency,damping=0.,
|
||||
max_abs_y_D=float(np.abs(rows[:completed,1]).max()/D) if completed else None)
|
||||
(out/'report.json').write_text(json.dumps(report,indent=2,allow_nan=False)+'\n')
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--scene',choices=('steady','karman'),required=True)
|
||||
ap.add_argument('--out',type=Path,required=True)
|
||||
ap.add_argument('--model',type=Path)
|
||||
ap.add_argument('--norm',type=Path)
|
||||
ap.add_argument('--release-dir',type=Path)
|
||||
ap.add_argument('--smoke',action='store_true',help='3 x 32 steps, all switches, no physical result')
|
||||
ap.add_argument('--wall-seconds',type=float,default=10800.)
|
||||
args=ap.parse_args()
|
||||
if not np.isfinite(args.wall_seconds) or args.wall_seconds<=0:
|
||||
ap.error('finite positive wall cap required')
|
||||
if args.scene=='karman':
|
||||
if args.model is None or args.norm is None:
|
||||
ap.error('Karman requires the frozen model and norm')
|
||||
if base.sha(args.model)!=base.MODEL_SHA or base.sha(args.norm)!=base.NORM_SHA:
|
||||
ap.error('frozen Karman model/normalizer hash mismatch')
|
||||
if not args.smoke and args.release_dir is None:
|
||||
ap.error('physical Karman requires its verified static release')
|
||||
elif any(p is not None for p in (args.model,args.norm,args.release_dir)):
|
||||
ap.error('steady uses constant control and its own warmup, no borrowed policy or release')
|
||||
out=args.out.resolve()
|
||||
if out==ROOT or ROOT in out.parents:
|
||||
ap.error('output must be outside checkout')
|
||||
out.mkdir(parents=True,exist_ok=False)
|
||||
intervals,si,warm=timing(args.scene,args.smoke)
|
||||
cfgpath=out/'config.json'
|
||||
cfgpath.write_text(json.dumps(base.config(40),indent=2)+'\n')
|
||||
source=subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True).strip()
|
||||
case=dict(source=source,scene=args.scene,smoke=args.smoke,D=D,U=U,nu=.004,
|
||||
phases=PHASES,SI=si,intervals_per_phase=intervals,
|
||||
phase_duration_tU_D=intervals*si*U/D, mass_ratio=4.,fn_D_U=.2,zeta=0.,
|
||||
first_ppo_observation='previous complete OFF interval; no reset at switches',
|
||||
rhs='previous native step CME_y+Wen_y; no filtering; policy uses SI-mean CME',
|
||||
control='fixed (0,-5.1,+5.1)U' if args.scene=='steady' else 'frozen karman_re100 PPO; actual Re_D50',
|
||||
model_sha256=base.sha(args.model) if args.model else None,
|
||||
norm_sha256=base.sha(args.norm) if args.norm else None,
|
||||
config_sha256=base.sha(cfgpath),render='sparse post-step snapshots only; no step skipping')
|
||||
model=base.load_policy(args.model) if args.model else None
|
||||
norm=json.loads(args.norm.read_text()) if args.norm else None
|
||||
seed=None
|
||||
if args.scene=='karman' and not args.smoke:
|
||||
seed,force=base.saved_release(args.release_dir)
|
||||
case['release_sha256']=base.sha(args.release_dir/'release.npz')
|
||||
(out/'case.json').write_text(json.dumps(case,indent=2)+'\n')
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.body.motion import MovingTables
|
||||
started=time.monotonic()
|
||||
structure=base.Structure(4.,2.5e-5,0.,D)
|
||||
video=None
|
||||
try:
|
||||
with Simulation(str(cfgpath)) as sim:
|
||||
probes,bodies=add_scene(sim,args.scene)
|
||||
sim.initialize()
|
||||
if seed is not None:
|
||||
case['release_transfer']=base.install_seed(sim,seed)
|
||||
del seed
|
||||
else:
|
||||
base.warmup(sim,warm-1,started,args.wall_seconds)
|
||||
sim.run(1)
|
||||
d=sim.moving_diagnostics()
|
||||
force=float(d[:,MovingTables.DIAG.index('cme_y')].sum(dtype=np.float64)
|
||||
+d[:,MovingTables.DIAG.index('wen_y')].sum(dtype=np.float64))
|
||||
case.update(warmup_executed=0 if 'release_transfer' in case else warm,
|
||||
origin_clock=sim.stepper.step_count)
|
||||
(out/'case.json').write_text(json.dumps(case,indent=2)+'\n')
|
||||
phase_steps=intervals*si
|
||||
video=Video(out,args.scene,bodies,phase_steps,smoke=args.smoke,source=source)
|
||||
evolve(sim,probes,bodies,structure,force,scene=args.scene,model=model,norm=norm,
|
||||
intervals=intervals,si=si,out=out,render=video.capture,
|
||||
start=started,wall_cap=args.wall_seconds)
|
||||
finally:
|
||||
if video is not None:
|
||||
video.close()
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
@@ -1,224 +0,0 @@
|
||||
"""Ledger, settling, common window and Morison fits for Dütsch cases (numpy only).
|
||||
|
||||
python analyze.py CASE_DIR [CASE_DIR ...] # cases of one comparison
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
import protocol as p
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(os.path.join(path, "case.json")) as fh:
|
||||
case = json.load(fh)
|
||||
raw = np.load(os.path.join(path, "raw.npz"))
|
||||
case.update(force=raw["force"], diag=raw["diag"], samples=raw["samples"], path=path)
|
||||
return case
|
||||
|
||||
|
||||
def cycle_slices(case):
|
||||
"""{k: slice of step indices m whose interval [m, m+1] lies in paper cycle k}."""
|
||||
T, q = case["T"], case["T"] // 4
|
||||
return {k: slice(q + (k - 1) * T, q + k * T)
|
||||
for k in range(1, case["cycles"] + 1) if q + k * T <= len(case["force"])}
|
||||
|
||||
|
||||
def coarse_wave(case, k, r=1):
|
||||
"""Normalized per-step Fx of cycle k, averaged over r native steps per sample."""
|
||||
f = case["force"][cycle_slices(case)[k], 0] / case["F_ref"]
|
||||
return f.reshape(-1, r).mean(axis=1)
|
||||
|
||||
|
||||
def profiles(case, k):
|
||||
"""{(phase, station, x2): (u, v)} of cycle k, time-averaged over the bin steps
|
||||
(equal weights per step) from valid samples only."""
|
||||
s = case["samples"]
|
||||
acc = {}
|
||||
for _, _, ph, xs, x2, u, v, _ in s[s[:, 1] == k]:
|
||||
if not (math.isnan(u) or math.isnan(v)):
|
||||
acc.setdefault((ph, xs, x2), []).append((u, v))
|
||||
return {key: tuple(np.mean(vals, axis=0)) for key, vals in acc.items()}
|
||||
|
||||
|
||||
def coverage(case, k):
|
||||
s = case["samples"]
|
||||
rows = s[s[:, 1] == k]
|
||||
missing = int(np.isnan(rows[:, 5]).sum())
|
||||
return dict(samples=len(rows), missing=missing, bin_steps=len(set(rows[:, 0])),
|
||||
min_fluid_weight=float(np.nanmin(np.where(rows[:, 7] > 0, rows[:, 7], np.nan)))
|
||||
if len(rows) else math.nan)
|
||||
|
||||
|
||||
def common_points(dicts):
|
||||
keys = set(dicts[0])
|
||||
for d in dicts[1:]:
|
||||
keys &= set(d)
|
||||
return sorted(keys)
|
||||
|
||||
|
||||
def candidates(case):
|
||||
"""Per-step Fx of each reported force candidate (none is selected as an RHS)."""
|
||||
d = case["diag"]
|
||||
return {"cme": d[:, 0], "cme_wen": d[:, 0] + d[:, 2], "cme_node": d[:, 0] + d[:, 6] - d[:, 9]}
|
||||
|
||||
|
||||
def cycle_morison(case, ks, fx=None):
|
||||
T = case["T"]
|
||||
fx = case["force"][:, 0] if fx is None else fx
|
||||
idx = np.concatenate([np.arange(len(fx))[cycle_slices(case)[k]] for k in ks])
|
||||
b = np.array([p.interval_bases(int(m), T) for m in idx])
|
||||
return p.morison_fit(fx[idx] / case["F_ref"], b[:, 0], b[:, 1])
|
||||
|
||||
|
||||
def pair_metrics(case, k):
|
||||
a, b = coarse_wave(case, k - 1), coarse_wave(case, k)
|
||||
pa, pb = profiles(case, k - 1), profiles(case, k)
|
||||
common = common_points([pa, pb]) # empty set -> inf -> pair fails (FAIL-CLOSED)
|
||||
prof = max((max(abs(pa[c][0] - pb[c][0]), abs(pa[c][1] - pb[c][1])) for c in common),
|
||||
default=math.inf) / case["u_max"]
|
||||
(cd0, ci0), (cd1, ci1) = cycle_morison(case, [k - 1]), cycle_morison(case, [k])
|
||||
m = dict(wave=float(np.sqrt(np.mean((b - a) ** 2))),
|
||||
scalar=float(max(abs(b.mean() - a.mean()), abs(np.sqrt(np.mean(b * b)) - np.sqrt(np.mean(a * a))),
|
||||
abs(np.abs(b).max() - np.abs(a).max()))),
|
||||
profile=float(prof), morison=float(max(abs(cd1 - cd0), abs(ci1 - ci0))),
|
||||
profile_points=len(common))
|
||||
m["pass"] = all(m[key] <= tol for key, tol in p.SETTLE.items())
|
||||
return m
|
||||
|
||||
|
||||
def hf_component(case, ks):
|
||||
"""Unqualified phase-locked high-frequency force component (reported, not checked):
|
||||
mean CME waveform over cycles ks minus its two-basis Morison reconstruction,
|
||||
split at 20/T; cycle differencing does not remove it."""
|
||||
T = case["T"]
|
||||
if not ks:
|
||||
return dict(label="unqualified phase-locked HF force component, not checked", cycles=[])
|
||||
mean = np.mean([coarse_wave(case, k) for k in ks], axis=0)
|
||||
c_d, c_i = cycle_morison(case, ks)
|
||||
q = T // 4
|
||||
b = np.array([p.interval_bases(q + m, T) for m in range(T)])
|
||||
resid = mean - (c_d * b[:, 0] - (np.pi ** 2 * c_i / p.KC) * b[:, 1])
|
||||
spec = np.fft.rfft(resid)
|
||||
high = spec.copy()
|
||||
high[:21] = 0.0
|
||||
energy = one_sided_energy(spec, T)
|
||||
return dict(label="unqualified phase-locked HF force component, not checked",
|
||||
cycles=list(ks), hf_rms_over_F_ref=float(np.sqrt(np.mean(np.fft.irfft(high, T) ** 2))),
|
||||
hf_energy_fraction=float(energy[21:].sum() / max(energy.sum(), 1e-300)),
|
||||
top_harmonics=[int(j) for j in np.argsort(energy[21:])[::-1][:5] + 21])
|
||||
|
||||
|
||||
def one_sided_energy(spec, n):
|
||||
"""Parseval-consistent energy per rfft bin: interior bins count twice, DC and
|
||||
(for even n) Nyquist once."""
|
||||
w = np.full(len(spec), 2.0)
|
||||
w[0] = 1.0
|
||||
if n % 2 == 0:
|
||||
w[-1] = 1.0
|
||||
return w * np.abs(spec) ** 2
|
||||
|
||||
|
||||
def ledger(case, horizon=p.LAST_SCORED):
|
||||
ks = [k for k in sorted(cycle_slices(case)) if k <= horizon]
|
||||
pairs = {k: pair_metrics(case, k) for k in ks if k - 1 > p.DISCARD}
|
||||
return pairs, p.settled_start({k: v["pass"] for k, v in pairs.items()})
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""argv: CASE_DIR ... [--reference CASE_DIR]; the reference defaults to the
|
||||
largest D (resolution ladder); domain runs name the 60 D case explicitly."""
|
||||
ref_path = None
|
||||
if "--reference" in argv:
|
||||
i = argv.index("--reference")
|
||||
ref_path, argv = argv[i + 1], argv[:i] + argv[i + 2:]
|
||||
horizon, explicit = p.LAST_SCORED, "--horizon" in argv # default: original scoring
|
||||
if explicit:
|
||||
i = argv.index("--horizon")
|
||||
horizon, argv = int(argv[i + 1]), argv[:i] + argv[i + 2:]
|
||||
paths = argv if ref_path is None or ref_path in argv else argv + [ref_path]
|
||||
cases = [load(path) for path in paths]
|
||||
for c in cases if explicit else (): # complete cycles 1..N; never pad or shift
|
||||
if c["cycles"] < horizon or len(c["force"]) < round((0.25 + horizon) * c["T"]):
|
||||
raise SystemExit(f"{c['path']}: run does not contain complete cycles 1..{horizon}")
|
||||
ledgers = [ledger(c, horizon) for c in cases]
|
||||
window = p.common_window([k for _, k in ledgers],
|
||||
[{k: v["pass"] for k, v in pairs.items()} for pairs, _ in ledgers],
|
||||
last_scored=horizon)
|
||||
report = dict(window=window, horizon=horizon, cases=[],
|
||||
horizon_mode="default-cap" if horizon == p.LAST_SCORED else
|
||||
"extended duration diagnostic (declared amendment, D40/60D only)")
|
||||
for case, (pairs, k_case) in zip(cases, ledgers):
|
||||
limit = round((0.25 + horizon) * case["T"]) # events inside the horizon only
|
||||
entry = {key: case[key] for key in ("path", "D", "T", "box", "n", "u_max", "nu", "tau", "A",
|
||||
"steps", "cycles", "F_ref")}
|
||||
entry.update(duration_T=case["steps"] / case["T"], K_case=k_case, pairs=pairs,
|
||||
coverage={k: coverage(case, k) for k in sorted(cycle_slices(case))
|
||||
if p.DISCARD < k <= horizon},
|
||||
no_donor=float(case["diag"][:limit, 13].sum()),
|
||||
eq_only=float(case["diag"][:limit, 12].sum()),
|
||||
hf_component=hf_component(case, window or tuple(
|
||||
[k for k in sorted(cycle_slices(case)) if p.DISCARD < k <= horizon][-3:])))
|
||||
if window: # residual drift inside the scored window (pairs K..K+2)
|
||||
entry["window_drift"] = {key: max(pairs[k][key] for k in window)
|
||||
for key in ("wave", "profile", "morison")}
|
||||
entry["morison_obs"] = cycle_morison(case, window)
|
||||
entry["morison"] = {name: cycle_morison(case, window, fx)
|
||||
for name, fx in candidates(case).items()}
|
||||
entry["fy_mean"] = float(np.mean(np.concatenate(
|
||||
[case["force"][cycle_slices(case)[k], 1] for k in window])) / case["F_ref"])
|
||||
report["cases"].append(entry)
|
||||
if window and len(cases) > 1: # cross-resolution waveform against the finest case
|
||||
fine = (next(c for c in cases if c["path"] == ref_path) if ref_path
|
||||
else max(cases, key=lambda c: c["D"]))
|
||||
report["reference"] = fine["path"]
|
||||
ref = np.concatenate([coarse_wave(fine, k) for k in window])
|
||||
for case, entry in zip(cases, report["cases"]):
|
||||
r = fine["T"] // case["T"] # 1 for equal-D domain runs
|
||||
ref_c = ref.reshape(-1, r).mean(axis=1)
|
||||
own = np.concatenate([coarse_wave(case, k) for k in window])
|
||||
entry["wave_rms_vs_finest"] = float(np.sqrt(np.mean((own - ref_c) ** 2)))
|
||||
for k in window: # profiles on the common valid point set of all compared cases
|
||||
per_case = [profiles(c, k) for c in cases]
|
||||
keys = common_points(per_case)
|
||||
report.setdefault("common_profile_points", {})[k] = len(keys)
|
||||
if not keys:
|
||||
report["verdict"] = "FAIL-CLOSED: empty common profile set"
|
||||
continue
|
||||
fine_p = per_case[cases.index(fine)]
|
||||
for case, entry, prof in zip(cases, report["cases"], per_case):
|
||||
d = max(max(abs(prof[key][0] - fine_p[key][0]), abs(prof[key][1] - fine_p[key][1]))
|
||||
for key in keys) / case["u_max"]
|
||||
entry.setdefault("profile_max_vs_finest", {})[k] = float(d)
|
||||
ref_entry = report["cases"][cases.index(fine)]
|
||||
for entry in report["cases"]: # conservative: difference + both drifts
|
||||
if entry is ref_entry:
|
||||
continue
|
||||
drift = {key: entry["window_drift"][key] + ref_entry["window_drift"][key]
|
||||
for key in ("wave", "profile", "morison")}
|
||||
dm = max(abs(entry["morison"]["cme"][0] - ref_entry["morison"]["cme"][0]),
|
||||
abs(entry["morison"]["cme"][1] - ref_entry["morison"]["cme"][1]))
|
||||
entry["conservative"] = dict(
|
||||
wave=entry["wave_rms_vs_finest"] + drift["wave"],
|
||||
profile=max(entry.get("profile_max_vs_finest", {}).values(), default=math.inf)
|
||||
+ drift["profile"],
|
||||
morison_cme=dm + drift["morison"])
|
||||
bands = p.DOMAIN if ref_path else p.CROSS # domain runs name the 60 D reference
|
||||
c = entry["conservative"]
|
||||
entry["pass_scope"] = (
|
||||
("wave+profile+morison_cme" if "morison" in bands else "wave+profile")
|
||||
+ ": conservative (|diff| + both window drifts) on the obs/CME Fx waveform and "
|
||||
"simulation-to-simulation profiles; CME+Wen and CME+node reported, not checked; "
|
||||
"no Fig. 8 / literature verdict")
|
||||
entry["pass"] = (c["wave"] <= bands["wave"] and c["profile"] <= bands["profile"]
|
||||
and ("morison" not in bands or c["morison_cme"] <= bands["morison"]))
|
||||
print(json.dumps(report, indent=1, default=float))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
@@ -1,52 +0,0 @@
|
||||
"""Research-only field capture helpers for the Dütsch driver (numpy only).
|
||||
|
||||
Snapshots are taken at the first step of the scored phase bins in declared
|
||||
cycles: a near-body crop at full resolution and a fluid-weighted block-mean
|
||||
downsample of the whole domain; plus scalar trends at every bin read.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
CROP_X, CROP_Y, DOWN = 8.0, 6.0, 8 # crop half-widths in D; downsample factor
|
||||
STRIP = 2.0 # inlet/outlet strip width in D
|
||||
|
||||
|
||||
def crop_box(xm, ym, D, n, body_extent):
|
||||
"""Inclusive integer crop (x0, x1, y0, y1); fail closed if it leaves the domain
|
||||
interior or does not contain the body's swept extent (x_lo, x_hi, y_lo, y_hi)."""
|
||||
x0, x1 = int(round(xm - CROP_X * D)), int(round(xm + CROP_X * D))
|
||||
y0, y1 = int(round(ym - CROP_Y * D)), int(round(ym + CROP_Y * D))
|
||||
if x0 < 1 or y0 < 1 or x1 > n - 2 or y1 > n - 2:
|
||||
raise ValueError("capture crop leaves the domain interior")
|
||||
bx0, bx1, by0, by1 = body_extent
|
||||
if not (x0 <= bx0 and bx1 <= x1 and y0 <= by0 and by1 <= y1):
|
||||
raise ValueError("capture crop clips the body's swept extent")
|
||||
return x0, x1, y0, y1
|
||||
|
||||
|
||||
def downsample(field, fluid, f=DOWN):
|
||||
"""Fluid-weighted f x f block means; NaN where a block has no fluid cell."""
|
||||
ny, nx = field.shape
|
||||
h, w = ny // f, nx // f
|
||||
v = np.where(fluid, field, 0.0)[:h * f, :w * f].reshape(h, f, w, f).sum(axis=(1, 3))
|
||||
c = fluid[:h * f, :w * f].reshape(h, f, w, f).sum(axis=(1, 3))
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
return np.where(c > 0, v / np.maximum(c, 1), np.nan).astype(np.float32)
|
||||
|
||||
|
||||
def fluid_mean(field, fluid, xs=slice(None), ys=slice(None)):
|
||||
m = fluid[ys, xs]
|
||||
return float(field[ys, xs][m].mean()) if m.any() else float("nan")
|
||||
|
||||
|
||||
def trend_row(s, k, ph, rho, ux, uy, fluid, D, n, crop):
|
||||
"""(s, cycle, phase, <rho>, <ux>, <uy>, <rho>_crop, inlet <rho>, <ux>, outlet <rho>, <ux>)."""
|
||||
w = max(1, int(round(STRIP * D)))
|
||||
x0, x1, y0, y1 = crop
|
||||
cx, cy = slice(x0, x1 + 1), slice(y0, y1 + 1)
|
||||
inlet, outlet, inner = slice(1, 1 + w), slice(n - 1 - w, n - 1), slice(1, n - 1)
|
||||
return (s, k, ph, fluid_mean(rho, fluid), fluid_mean(ux, fluid), fluid_mean(uy, fluid),
|
||||
fluid_mean(rho, fluid, cx, cy),
|
||||
fluid_mean(rho, fluid, inlet, inner), fluid_mean(ux, fluid, inlet, inner),
|
||||
fluid_mean(rho, fluid, outlet, inner), fluid_mean(ux, fluid, outlet, inner))
|
||||
@@ -1,136 +0,0 @@
|
||||
"""Pure arithmetic of the Dütsch Re 100 / KC 5 protocol (numpy only, no CUDA).
|
||||
|
||||
docs/validation/duetsch_re100_kc5_protocol.md is the contract: elapsed time s
|
||||
in native steps from the right turning point, paper phase
|
||||
phi = (360 s / T - 90) mod 360, paper cycle k >= 1 spans
|
||||
s/T in [0.25 + (k-1), 0.25 + k); cycle 0 is the startup quarter.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from functools import lru_cache
|
||||
|
||||
import numpy as np
|
||||
|
||||
KC, RE = 5.0, 100.0
|
||||
STATIONS = (-0.6, 0.0, 0.6, 1.2) # lab-fixed x/D from the mean position
|
||||
PHASES = (180.0, 210.0, 330.0) # degrees; bins [p, p + 1)
|
||||
X2 = tuple(np.round(np.arange(-1.5, 1.5001, 0.05), 10)) # declared x2/D points
|
||||
DISCARD, LAST_SCORED, N_PAIRS = 2, 11, 3 # cycles 0..2 discarded, cap 12 T
|
||||
# A-priori bands. Settling is one fifth of the tightest comparison band of the
|
||||
# same observable, and comparisons add both cases' residual window drift
|
||||
# (conservative: |difference| + drift_a + drift_b <= band).
|
||||
DOMAIN = dict(wave=.01, morison=.02, profile=.02) # vs the 60 D reference at D = 40
|
||||
CROSS = dict(wave=.02, profile=.02) # D40 vs D80
|
||||
SETTLE = dict(wave=.002, scalar=.002, profile=.004, morison=.004)
|
||||
|
||||
|
||||
def amplitude(D):
|
||||
return KC * D / (2.0 * math.pi)
|
||||
|
||||
|
||||
def position(s, T, A):
|
||||
"""Centre position relative to the mean: x = A cos(2 pi s / T), evaluated from the
|
||||
native step modulo T so every cycle repeats the same geometry bit for bit."""
|
||||
return A * math.cos(2.0 * math.pi * (int(s) % T) / T)
|
||||
|
||||
|
||||
def phase(s, T):
|
||||
"""Paper phase in degrees from the native step modulo T (the unwrapped cycle
|
||||
index is kept separately by ``cycle``)."""
|
||||
return (360.0 * (int(s) % T) / T - 90.0) % 360.0
|
||||
|
||||
|
||||
def cycle(s, T):
|
||||
return 0 if s < 0.25 * T else int(math.floor(s / T - 0.25)) + 1
|
||||
|
||||
|
||||
def bin_phase(s, T):
|
||||
"""Scored phase whose 1-degree bin [p, p + 1) contains time s, else None."""
|
||||
phi = phase(s, T)
|
||||
for p in PHASES:
|
||||
if p <= phi < p + 1.0:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _quadrature_rule(points):
|
||||
"""Reuse the nodes and weights, which depend only on quadrature order."""
|
||||
return np.polynomial.legendre.leggauss(points)
|
||||
|
||||
|
||||
def interval_bases(n, T, points=16):
|
||||
"""Single-step interval averages over [n, n + 1] of cos phi |cos phi| and sin phi
|
||||
(phi in radians = 2 pi s / T - pi / 2), Gauss-Legendre quadrature."""
|
||||
x, w = _quadrature_rule(points)
|
||||
s = int(n) % T + 0.5 * (x + 1.0) # periodic: identical bases in every cycle
|
||||
phi = 2.0 * np.pi * s / T - 0.5 * np.pi
|
||||
c = np.cos(phi)
|
||||
return 0.5 * float(np.sum(w * c * np.abs(c))), 0.5 * float(np.sum(w * np.sin(phi)))
|
||||
|
||||
|
||||
def morison_fit(f_norm, b1, b2):
|
||||
"""Least squares of f_norm = c_d b1 - (pi^2 c_i / KC) b2 (paper phase, no shift)."""
|
||||
a = np.column_stack([b1, -(math.pi ** 2 / KC) * np.asarray(b2)])
|
||||
(c_d, c_i), *_ = np.linalg.lstsq(a, np.asarray(f_norm), rcond=None)
|
||||
return float(c_d), float(c_i)
|
||||
|
||||
|
||||
def inside_circle(x, y, centre, radius):
|
||||
return (x - centre[0]) ** 2 + (y - centre[1]) ** 2 < radius * radius
|
||||
|
||||
|
||||
def sample(field, fluid, x, y, centre, radius):
|
||||
"""Profile sample: MISSING if the exact point is inside the body (checked before
|
||||
any corner interpolation), else ``bilinear_fluid``."""
|
||||
if inside_circle(x, y, centre, radius):
|
||||
return None, (), 0.0
|
||||
return bilinear_fluid(field, fluid, x, y)
|
||||
|
||||
|
||||
def bilinear_fluid(field, fluid, x, y):
|
||||
"""Bilinear value at (x, y) in cell coordinates from fluid cells only.
|
||||
|
||||
Returns (value, ((ix, iy, weight), ...), fluid_weight_sum) with weights
|
||||
renormalized over the fluid corners and the original sum of their bilinear
|
||||
weights, or (None, (), 0.0) when no corner is fluid (MISSING, never zero).
|
||||
"""
|
||||
x0, y0 = int(math.floor(x)), int(math.floor(y))
|
||||
fx, fy = x - x0, y - y0
|
||||
corners = ((x0, y0, (1 - fx) * (1 - fy)), (x0 + 1, y0, fx * (1 - fy)),
|
||||
(x0, y0 + 1, (1 - fx) * fy), (x0 + 1, y0 + 1, fx * fy))
|
||||
used = [(i, j, w) for i, j, w in corners if w > 0.0 and fluid[j, i]]
|
||||
total = sum(w for _, _, w in used)
|
||||
if total == 0.0:
|
||||
return None, (), 0.0
|
||||
used = tuple((i, j, w / total) for i, j, w in used)
|
||||
return float(sum(w * field[j, i] for i, j, w in used)), used, float(total)
|
||||
|
||||
|
||||
def settled_start(pairs_pass):
|
||||
"""K_case from {k: pass of pair (k-1 -> k)}: first cycle after three consecutive
|
||||
passing pairs among cycles > DISCARD, or None."""
|
||||
run = 0
|
||||
for k in sorted(pairs_pass):
|
||||
if k - 1 <= DISCARD:
|
||||
continue
|
||||
run = run + 1 if pairs_pass[k] else 0
|
||||
if run == N_PAIRS:
|
||||
return k + 1
|
||||
return None
|
||||
|
||||
|
||||
def common_window(k_cases, pairs_pass_by_case, last_scored=LAST_SCORED):
|
||||
"""Common scored cycles K..K+2 with K = max K_case, or None (FAIL-CLOSED) when a
|
||||
case never settled, K + 2 exceeds the cap, or a case fails any pair p_j for
|
||||
j = its own K_case .. K + 2 (continuous settling, including the wait for K)."""
|
||||
if any(k is None for k in k_cases):
|
||||
return None
|
||||
K = max(k_cases)
|
||||
if K + 2 > last_scored:
|
||||
return None
|
||||
for k_case, pairs in zip(k_cases, pairs_pass_by_case):
|
||||
if not all(pairs.get(j, False) for j in range(k_case, K + 3)):
|
||||
return None
|
||||
return K, K + 1, K + 2
|
||||
@@ -1,133 +0,0 @@
|
||||
"""One-shot Dütsch Re 100 / KC 5 prescribed in-line oscillation driver.
|
||||
|
||||
Protocol: docs/validation/duetsch_re100_kc5_protocol.md. Native dense channel
|
||||
(zero-velocity inlet, pressure-NEQ outlet, bounce-back y walls), MRT, lab frame.
|
||||
Writes raw per-step force/diagnostics and phase-bin profile samples to --out
|
||||
(outside the checkout); analysis is in analyze.py. No production change.
|
||||
|
||||
python run.py --D 40 --box 40 --cycles 11 --out /path/outside/checkout
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
|
||||
import protocol as p
|
||||
|
||||
|
||||
TEMPLATE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "src",
|
||||
"CelerisLab", "configs", "config_lbm.json")
|
||||
|
||||
|
||||
def config(D, box, T):
|
||||
"""Shipped config_lbm.json (full parser schema) with only the case fields
|
||||
overridden: grid, still fluid (velocity 0), nu from Re, MRT, uniform inlet
|
||||
profile, bounce-back y walls. Inlet scheme and outlet mode stay the
|
||||
template's (zou_he_local, neq_extrap) and are recorded in case.json."""
|
||||
u = p.KC * D / T
|
||||
n = int(round(box * D))
|
||||
with open(TEMPLATE) as fh:
|
||||
cfg = json.load(fh)
|
||||
cfg["grid"].update(lattice_model="D2Q9", nx=n, ny=n, nz=1)
|
||||
cfg["physics"].update(data_type="FP32", viscosity=u * D / p.RE, velocity=0.0, rho=1.0)
|
||||
cfg["method"].update(collision="MRT", streaming="double_buffer", store_precision="FP32",
|
||||
ddf_shifting=False, y_wall_bc="bounce_back")
|
||||
cfg["method"]["les"]["enabled"] = False
|
||||
cfg["method"]["inlet"]["profile"] = "uniform"
|
||||
return cfg, u, n
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--D", type=int, required=True)
|
||||
ap.add_argument("--box", type=float, default=40.0, help="box side in D")
|
||||
ap.add_argument("--T", type=int, default=None, help="period in steps (multiple of 360)")
|
||||
ap.add_argument("--cycles", type=int, default=p.LAST_SCORED, help="run to the end of this cycle")
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--dump-fields", default="",
|
||||
help="research capture: comma-separated paper cycles, e.g. 11,21,30,31")
|
||||
args = ap.parse_args()
|
||||
D, T = args.D, args.T or 126 * args.D
|
||||
if T % 360:
|
||||
raise SystemExit("T must be a multiple of 360 steps")
|
||||
if os.path.realpath(args.out).startswith(os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))):
|
||||
raise SystemExit("--out must be outside the checkout")
|
||||
cfg, u_max, n = config(D, args.box, T)
|
||||
A, R = p.amplitude(D), 0.5 * D
|
||||
xm = ym = 0.5 * (n - 1)
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
from CelerisLab.body.motion import Translation
|
||||
from CelerisLab.simulation import Simulation
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
|
||||
json.dump(cfg, fh)
|
||||
sim = Simulation(fh.name)
|
||||
try:
|
||||
body = sim.add_body("circle", (xm + A, ym), R,
|
||||
translation=Translation((-2.0 * A, 0.0, 0.0, 0.0), 1.01 * u_max))
|
||||
sim.initialize()
|
||||
dump = {int(c) for c in args.dump_fields.split(",") if c}
|
||||
if dump:
|
||||
import capture
|
||||
if max(dump) > args.cycles or min(dump) <= p.DISCARD:
|
||||
raise SystemExit("--dump-fields cycles must lie in the scored range of this run")
|
||||
crop = capture.crop_box(xm, ym, D, n, (xm - A - R, xm + A + R, ym - R, ym + R))
|
||||
trend = []
|
||||
steps = int(round((0.25 + args.cycles) * T))
|
||||
force = np.zeros((steps, 2)); diag = []; samples = []
|
||||
for m in range(steps):
|
||||
x0, x1 = p.position(m, T, A), p.position(m + 1, T, A)
|
||||
sim.set_body(body, offset=(max(x1 - A, -2.0 * A), 0.0), vx=x1 - x0)
|
||||
sim.run(1)
|
||||
force[m] = sim.read_force(body)[:2]
|
||||
diag.append(sim.moving_diagnostics()[0])
|
||||
s = m + 1 # state after the step
|
||||
ph = p.bin_phase(s, T)
|
||||
if ph is not None and p.cycle(s, T) > p.DISCARD:
|
||||
fields = sim.get_macroscopic()
|
||||
fluid = (sim.get_flags().reshape(n, n) & 1) != 0
|
||||
ux, uy = (np.asarray(fields[k]).reshape(n, n) for k in ("ux", "uy"))
|
||||
if dump: # research capture only; the default path is unchanged
|
||||
rho = np.asarray(fields["rho"]).reshape(n, n)
|
||||
trend.append(capture.trend_row(s, p.cycle(s, T), ph, rho, ux, uy, fluid, D, n, crop))
|
||||
if p.cycle(s, T) in dump and p.phase(s, T) == ph: # first step of the bin
|
||||
x0c, x1c, y0c, y1c = crop
|
||||
cy_, cx_ = slice(y0c, y1c + 1), slice(x0c, x1c + 1)
|
||||
np.savez_compressed(
|
||||
os.path.join(args.out, f"fields_c{p.cycle(s, T)}_p{int(ph)}.npz"),
|
||||
s=s, cycle=p.cycle(s, T), phase=ph, crop=np.asarray(crop),
|
||||
force=force[m], centre=np.asarray((xm + p.position(s, T, A), ym)),
|
||||
ux=ux[cy_, cx_].astype(np.float32), uy=uy[cy_, cx_].astype(np.float32),
|
||||
rho=rho[cy_, cx_].astype(np.float32), fluid=fluid[cy_, cx_].astype(np.uint8),
|
||||
down_ux=capture.downsample(ux, fluid), down_uy=capture.downsample(uy, fluid),
|
||||
down_rho=capture.downsample(rho, fluid))
|
||||
centre = (xm + p.position(s, T, A), ym) # analytic centre after the step
|
||||
for xs in p.STATIONS:
|
||||
for x2 in p.X2:
|
||||
x, y = xm + xs * D, ym + x2 * D
|
||||
u, wu, wsum = p.sample(ux, fluid, x, y, centre, R)
|
||||
v, _, _ = p.sample(uy, fluid, x, y, centre, R)
|
||||
samples.append((s, p.cycle(s, T), ph, xs, x2,
|
||||
math.nan if u is None else u, math.nan if v is None else v,
|
||||
wsum, json.dumps(wu)))
|
||||
if dump:
|
||||
np.save(os.path.join(args.out, "trend.npy"), np.asarray(trend, float))
|
||||
np.savez(os.path.join(args.out, "raw.npz"), force=force, diag=np.asarray(diag),
|
||||
samples=np.asarray([r[:8] for r in samples], float))
|
||||
with open(os.path.join(args.out, "case.json"), "w") as fh:
|
||||
json.dump(dict(D=D, T=T, box=args.box, n=n, u_max=u_max, nu=cfg["physics"]["viscosity"],
|
||||
tau=3 * cfg["physics"]["viscosity"] + 0.5, A=A, steps=steps,
|
||||
cycles=args.cycles, F_ref=0.5 * u_max ** 2 * D,
|
||||
inlet=cfg["method"]["inlet"], outlet=cfg["method"]["outlet"],
|
||||
weights=[r[8] for r in samples]), fh)
|
||||
finally:
|
||||
sim.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,130 +0,0 @@
|
||||
"""Offline Fig2 estimands; preserve raw same-clock errors beside registration.
|
||||
|
||||
No thresholds are fitted here. Probe DTW is the actual Legacy helper, not a
|
||||
field metric; independent wake phase is a diagnostic, not a time correction.
|
||||
"""
|
||||
import argparse
|
||||
import gc
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0,str(ROOT))
|
||||
from research.fig2_collection import run
|
||||
from research.fig2_materials.metrics import field_error, motion_summary
|
||||
|
||||
|
||||
def frequency(signal,dt):
|
||||
"""Largest non-DC rectangular-window FFT bin, descriptive resolution only."""
|
||||
a=np.asarray(signal,float);a=a-a.mean()
|
||||
if len(a)<3 or np.max(np.abs(a))<1e-12:return None
|
||||
power=np.abs(np.fft.rfft(a))**2
|
||||
i=1+int(np.argmax(power[1:]))
|
||||
return dict(f_D_U=float(np.fft.rfftfreq(len(a),dt)[i]),bin_width_D_U=1/(len(a)*dt),
|
||||
method='demeaned rectangular window; dominant non-DC bin; not a confidence interval')
|
||||
|
||||
|
||||
def native_summary(rows,scene):
|
||||
start=max(0,len(rows)-640000) if scene!='taylor' else 0
|
||||
a=rows[start:]
|
||||
y=a[:,1]/40.;cy=a[:,3:9].sum(axis=1)/(.5*.005**2*40)
|
||||
return dict(window_D_U=[start*.005/40,len(rows)*.005/40],
|
||||
motion=motion_summary(y),motion_frequency=frequency(y,.005/40),
|
||||
structural_RHS_Cy_RMS=float(np.sqrt(np.mean(cy**2))),
|
||||
structural_RHS_Cy_mean=float(cy.mean()),
|
||||
max_native_delta_Cy=float(np.abs(np.diff(cy)).max()) if len(cy)>1 else None,
|
||||
force_frequency=frequency(cy,.005/40),
|
||||
no_donor=float(rows[:,13].sum()),eq_only=float(rows[:,12].sum()),
|
||||
crossings=float(rows[:,14].sum()),
|
||||
force_convention='body force, sum pinball CME_y+Wen_y; native previous-step RHS, no filtering')
|
||||
|
||||
|
||||
def compare_selected(target,current,u0=.005):
|
||||
"""Fixed lab ROI; all actual compared roles must be fluid and finite here."""
|
||||
ys,xs=slice(311,711),slice(1360,2161)
|
||||
t,c=target['ux'][:,ys,xs],current['ux'][:,ys,xs]
|
||||
tv,cv=target['uy'][:,ys,xs],current['uy'][:,ys,xs]
|
||||
if t.shape!=c.shape or tv.shape!=cv.shape:raise ValueError('selected shape mismatch')
|
||||
for fields in (target,current):
|
||||
if not np.all((fields['flags'][:,ys,xs]&1)!=0):raise ValueError('nonfluid selected ROI')
|
||||
if not all(np.isfinite(a).all() for a in (t,c,tv,cv)):raise ValueError('nonfinite selected ROI')
|
||||
e2=((c.astype(float)-t)/u0)**2+((cv.astype(float)-tv)/u0)**2
|
||||
return dict(per_slot_E=np.sqrt(e2.mean(axis=(1,2))),aggregate_E=float(np.sqrt(e2.mean())))
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('directory',type=Path)
|
||||
args=ap.parse_args();p=args.directory
|
||||
case=json.loads((p/'case.json').read_text());term=json.loads((p/'terminal.json').read_text())
|
||||
if case['plan']['smoke'] or term['status']!='three_roles_completed_not_qualified':
|
||||
raise ValueError('physical analysis requires all three complete roles; smoke/partial is unscored')
|
||||
helpers=run.load_helpers()
|
||||
# Reuse the canonical pure module under the same isolated read-only package.
|
||||
native=run.load_file('_fig2_readonly_drl.legacy_test.metrics',
|
||||
run.ACQUISITION.parent/'legacy_test/metrics.py')
|
||||
reports={k:json.loads((p/k/'report.json').read_text()) for k in ('target','zero','controlled')}
|
||||
sig={}
|
||||
result=dict(scene=case['scene'],source=case['source'],Re_D=50.,roles={},
|
||||
primary='unaligned same-clock/event U-normalized vector errors; common lab ROI',
|
||||
periodic_phase='independently registered centre-uy phase diagnostic, not a shared-clock comparison',
|
||||
notes=['Modern moving-plant data; no Legacy numerical equivalence or physical qualification',
|
||||
'Motion, native structural force, policy SI means and probe DTW are different estimands'])
|
||||
for role in ('target','zero','controlled'):
|
||||
with np.load(p/role/'signals.npz') as z:sig[role]={k:z[k] for k in z.files}
|
||||
if role!='target':
|
||||
with np.load(p/role/'native.npz') as z:
|
||||
result['roles'][role]=native_summary(z['rows'],case['scene'])
|
||||
t=sig['target']['sensors']
|
||||
for role in ('zero','controlled'):
|
||||
s=sig[role]['sensors']
|
||||
if not np.array_equal(sig[role]['absolute_steps'],sig['target']['absolute_steps']):
|
||||
raise ValueError('probe absolute clocks mismatch')
|
||||
result['roles'][role]['unaligned_probe_area_velocity_RMS_over_U']=float(np.sqrt(np.mean(((s/156-t/156)/.005)**2)))
|
||||
result['roles'][role]['probe_channel_correlations']=native.channel_correlations(t,s)
|
||||
if case['scene']=='taylor':
|
||||
spec=types.SimpleNamespace(scene='vortex',conv_len=30)
|
||||
dtw=native.native_similarity(spec,t,s,len(s)-1)
|
||||
result['roles'][role]['native_probe_DTW']=dict(similarity=dtw,definition='source vortex training-window roll; supplementary, not event field recovery',conv_len=30)
|
||||
own=helpers.select_vortex_event(s,'vortex_taylor')
|
||||
target_event=helpers.select_vortex_event(t,'vortex_taylor')
|
||||
result['roles'][role]['own_event_peak']=int(own['peak_index'])
|
||||
result['roles'][role]['event_peak_delay_D_U']=(int(own['peak_index'])-int(target_event['peak_index']))*.4
|
||||
else:
|
||||
result['roles'][role]['native_probe_DTW']=native.cycle_similarity(t,s,30)
|
||||
result['roles'][role]['native_probe_DTW'].update(definition='canonical six-channel legacy cycle DTW, training D20 units',
|
||||
lag_column=1,target_template_indices=[30,60],state_window_indices=[len(s)-30,len(s)],
|
||||
registration_only=True,lag_identifiable=bool(case['scene']=='karman' and np.std(t[:,1])>1e-10))
|
||||
data={}
|
||||
for role in ('target','zero','controlled'):
|
||||
with np.load(p/role/'fields.npz') as z:data[role]={k:z[k] for k in z.files}
|
||||
for role in ('zero','controlled'):
|
||||
name={'karman':'independent_phase8_diagnostic','steady':'late_snapshot','taylor':'common_event8'}[case['scene']]
|
||||
if case['scene']!='karman' or all(len(data[k]['ux'])==8 for k in (role,'target')):
|
||||
result['roles'][role][name]=compare_selected(data['target'],data[role])
|
||||
else:
|
||||
result['roles'][role][name]=dict(status='unavailable_insufficient_periodic_crossings')
|
||||
result['roles'][role]['selection_metadata']=reports[role]['selection']
|
||||
result['roles'][role]['field_window_tU_D']=reports[role]['field_window_tU_D']
|
||||
if case['scene']=='karman' and all('cycle_mean_ux' in data[k] for k in (role,'target')):
|
||||
support=np.zeros(data[role]['cycle_mean_ux'].shape,bool);support[311:711,1360:2161]=True
|
||||
result['roles'][role]['E_complete_cycle_mean']=field_error(
|
||||
data[role]['cycle_mean_ux'],data[role]['cycle_mean_uy'],
|
||||
data['target']['cycle_mean_ux'],data['target']['cycle_mean_uy'],.005,support)
|
||||
if case['scene']=='taylor':
|
||||
clocks=[data[k]['absolute_steps'] for k in data]
|
||||
if not all(np.array_equal(clocks[0],v) for v in clocks[1:]):raise ValueError('event snapshot clocks differ')
|
||||
result['Taylor_injection']=dict(operator='all interior equilibrium rewrite; both buffers, identical event clock',
|
||||
strength=.000075,center=[600.,510.5],radius=80.,impulse_window='first two SI; retained native history',
|
||||
source_offsets=list(helpers.VORTEX_EVENT_OFFSETS))
|
||||
result['same_clock_field_errors']=json.loads((p/'same_clock_errors.json').read_text())
|
||||
run.write_json(p/'statistics.json',result)
|
||||
del data;gc.collect()
|
||||
print(json.dumps(dict(scene=case['scene'],statistics=str(p/'statistics.json'))))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -1,93 +0,0 @@
|
||||
"""Shared scientific plots of retained fields; no solver or synthetic fields."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import BoundaryNorm
|
||||
from matplotlib.patches import Circle,Rectangle
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0,str(ROOT))
|
||||
from research.continuous_viv.render import vorticity
|
||||
|
||||
|
||||
def decorate(ax,scene,role,y,*,mean=False):
|
||||
if not mean:
|
||||
if scene=='karman':ax.add_patch(Circle((-20,0),1,facecolor='white',edgecolor='#253544',lw=.7))
|
||||
if role!='target':
|
||||
for x,dy in ((0,0),(1.3,.75),(1.3,-.75)):
|
||||
ax.add_patch(Circle((x,dy+y),.5,facecolor='white',edgecolor='#253544',lw=.7))
|
||||
ax.add_patch(Rectangle((4,-5),20,10,fill=False,ls='--',lw=.6,edgecolor='#555'))
|
||||
ax.set_xlim(-23.6,27.6);ax.set_ylim(-6.4,6.4)
|
||||
ax.set_xlabel('(x - pinball-front equilibrium centre) / D',fontsize=8)
|
||||
ax.set_ylabel('(y - channel centre) / D',fontsize=8)
|
||||
ax.tick_params(labelsize=8)
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('directory',type=Path)
|
||||
args=ap.parse_args();p=args.directory
|
||||
case=json.loads((p/'case.json').read_text());scene=case['scene']
|
||||
cmap=plt.get_cmap('RdBu_r',21).copy();cmap.set_bad('#DDE2E5')
|
||||
norm=BoundaryNorm(np.linspace(-3,3,22),21,clip=True)
|
||||
prefix='SMOKE — plumbing only | ' if case['plan']['smoke'] else ''
|
||||
roles=('target','zero','controlled')
|
||||
data={};motion={}
|
||||
for role in roles:
|
||||
with np.load(p/role/'fields.npz') as z:data[role]={k:z[k] for k in z.files}
|
||||
with np.load(p/role/'native.npz') as z:
|
||||
motion[role]=z['rows'][:,1].copy()/40.
|
||||
shape=data['target']['ux'].shape[-2:]
|
||||
extent=((-0.5-1200)/40,(shape[1]-.5-1200)/40,
|
||||
(-.5-510.5)/40,(shape[0]-.5-510.5)/40)
|
||||
fig,axs=plt.subplots(3,1,figsize=(13,10),constrained_layout=True)
|
||||
for ax,role in zip(axs,roles):
|
||||
d=data[role];flags=d['common_fluid_all_boundaries'].astype(np.uint16)
|
||||
w=vorticity(d['fixed_window_mean_ux'],d['fixed_window_mean_uy'],flags)
|
||||
image=ax.imshow(w,origin='lower',extent=extent,cmap=cmap,norm=norm,interpolation='nearest')
|
||||
decorate(ax,scene,role,0,mean=True)
|
||||
label='no pinball reference' if role=='target' else ('zero rotation' if role=='zero' else ('constant rotations' if scene=='steady' else 'frozen PPO'))
|
||||
ax.set_title(f'{role}: {label} | arithmetic fixed-window mean',fontsize=10)
|
||||
fig.suptitle(prefix+f'{scene}: modern Re_D=50, m*=4, zero structural damping\n'
|
||||
'Gray: cells outside the common fluid support; mean fields are not instantaneous geometry',fontsize=12)
|
||||
fig.colorbar(image,ax=list(axs),shrink=.65,label='vorticity D/U, fixed limits [-3,3]')
|
||||
fig.savefig(p/'mean-flow.png',dpi=160);plt.close(fig)
|
||||
slots=max(len(data[r]['ux']) for r in roles)
|
||||
fig,axs=plt.subplots(3,slots,figsize=(max(8,3*slots),7),squeeze=False,constrained_layout=True)
|
||||
for row,role in enumerate(roles):
|
||||
d=data[role]
|
||||
for col,ax in enumerate(axs[row]):
|
||||
if col>=len(d['ux']):ax.axis('off');continue
|
||||
w=vorticity(d['ux'][col],d['uy'][col],d['flags'][col])
|
||||
image=ax.imshow(w,origin='lower',extent=extent,cmap=cmap,norm=norm,interpolation='nearest')
|
||||
step=int(d['selected_response_steps'][col]);y=float(motion[role][step-1])
|
||||
decorate(ax,scene,role,y)
|
||||
ax.set_xlim(-3,14);ax.set_ylim(-3,3)
|
||||
ax.set_xlabel('x/D relative to equilibrium front',fontsize=7)
|
||||
ax.set_title(f'{role} | slot {col+1}\ntU/D={step*.005/40:.2f}',fontsize=8)
|
||||
family={'karman':'independently phased diagnostic snapshots (different real times)',
|
||||
'steady':'late snapshots at matching real times',
|
||||
'taylor':'common event-relative snapshots at matching real times'}[scene]
|
||||
fig.suptitle(prefix+f'{scene}: {family}\nFixed lab coordinates; same vorticity normalization/limits; source '+case['source'][:8],fontsize=12)
|
||||
fig.colorbar(image,ax=list(axs.ravel()),shrink=.7,label='vorticity D/U [-3,3]')
|
||||
fig.savefig(p/'selected-flow.png',dpi=160);plt.close(fig)
|
||||
fig,ax=plt.subplots(figsize=(11,3.5),constrained_layout=True)
|
||||
for role,color in (('zero','#5A6570'),('controlled','#147D73')):
|
||||
y=motion[role];stride=max(1,len(y)//12000)
|
||||
time=(np.arange(len(y))+1)*.005/40
|
||||
ax.plot(time[::stride],y[::stride],lw=.8,label=role,color=color)
|
||||
ax.set(xlabel='response time tU/D',ylabel='transverse y/D',ylim=(-1,1))
|
||||
ax.legend();ax.grid(alpha=.2)
|
||||
ax.set_title(prefix+scene+': independent paired releases; full retained duration')
|
||||
fig.savefig(p/'motion.png',dpi=160);plt.close(fig)
|
||||
print(json.dumps(dict(scene=scene,plots=['mean-flow.png','selected-flow.png','motion.png'],
|
||||
vorticity_limits=[-3,3],normalization='curl(u) D/U; five-node fluid stencil',
|
||||
mean_mask='intersection over collected boundary fields',display_interpolation='nearest')))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -1,557 +0,0 @@
|
||||
"""Research-only Fig. 2 acquisition on the modern moving plant.
|
||||
|
||||
These paired releases are new experiments, not Legacy numerical reproductions.
|
||||
Nothing here qualifies a steady state or automatically extends/retunes a run.
|
||||
GPU dependencies are lazy so the collection contracts can be checked on CPU.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import gc
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
D, U, NU, SI = 40., .005, .004, 3200
|
||||
BASELINE = '76873d911c4e2caba1bacc25aaa1495ea768453f'
|
||||
CENTRES = ((1200., 510.5), (1252., 540.5), (1252., 480.5))
|
||||
PROBES = ((1600., 590.5), (1600., 510.5), (1600., 430.5))
|
||||
BIAS = np.array([0., -4., 4.])
|
||||
STEADY_SURFACE = np.array([0., -5.1, 5.1])*U
|
||||
MIN_AVAILABLE = 16*1024**3
|
||||
ACQUISITION = Path('/home/frank14f/DynamisLab/src/drl_pinball/acquisition.py')
|
||||
K_RELEASE = Path('/tmp/celeris-results-karman-viv-516975e/pair')
|
||||
ASSETS = {
|
||||
'karman': (Path('/tmp/celeris-karman-viv-assets/model.zip'),
|
||||
'148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d',
|
||||
Path('/tmp/celeris-karman-viv-assets/norm.json'),
|
||||
'534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0'),
|
||||
'taylor': (Path('/home/frank14f/DynamisLab/models/old/vortex_taylor.zip'),
|
||||
'58882721900f1eb0779f64aea0a2e233d307a94b640b9aaf33efa036db3c79e3',
|
||||
Path('/home/frank14f/DynamisLab/src/SR_analysis/data/vortex/vortex_taylor/norm.json'),
|
||||
'bd38b4cf2b6da56b5cf378e646d673b97a606531c68163460313b330c9957b86'),
|
||||
}
|
||||
NATIVE_COLUMNS = ('response_step', 'y', 'v', 'cme_F', 'cme_U', 'cme_L',
|
||||
'wen_F', 'wen_U', 'wen_L', 'node_F', 'node_U', 'node_L',
|
||||
'eq_only', 'no_donor', 'crossings', 'omega_F', 'omega_U', 'omega_L')
|
||||
|
||||
|
||||
def sha(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open('rb') as stream:
|
||||
for block in iter(lambda: stream.read(1024*1024), b''):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def json_value(value):
|
||||
if isinstance(value, dict):
|
||||
return {str(k): json_value(v) for k, v in value.items()}
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [json_value(v) for v in value]
|
||||
if isinstance(value, np.ndarray):
|
||||
return json_value(value.tolist())
|
||||
if isinstance(value, np.generic):
|
||||
return value.item()
|
||||
if isinstance(value, Path):
|
||||
return str(value.resolve())
|
||||
return value
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
Path(path).write_text(json.dumps(json_value(value), indent=2, allow_nan=False)+'\n')
|
||||
|
||||
|
||||
def load_file(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def load_helpers(path=ACQUISITION):
|
||||
"""Load read-only CPU helpers under an isolated package; no original imports.
|
||||
|
||||
Empty namespace packages avoid executing application __init__ modules. The
|
||||
acquisition module and its pure DTW dependency execute directly from source.
|
||||
Event functions are AST-selected verbatim to avoid acquire.py's heavy imports;
|
||||
offsets come from that acquisition module, never a copied/fabricated selector.
|
||||
"""
|
||||
path = Path(path)
|
||||
package = '_fig2_readonly_drl'
|
||||
for suffix, directory in (('', path.parent), ('.legacy_test', path.parent/'legacy_test'),
|
||||
('.legacy_test.core', path.parent/'legacy_test/core')):
|
||||
module = types.ModuleType(package+suffix)
|
||||
module.__path__ = [str(directory)]
|
||||
sys.modules[package+suffix] = module
|
||||
acquisition = load_file(package+'.acquisition', path)
|
||||
event_path = path.parent/'legacy_test/acquire.py'
|
||||
source = ast.parse(event_path.read_text(), filename=str(event_path))
|
||||
names = {'_vortex_event_channel', '_select_vortex_event'}
|
||||
nodes = [node for node in source.body if isinstance(node, ast.FunctionDef) and node.name in names]
|
||||
if {node.name for node in nodes} != names:
|
||||
raise ValueError('authoritative event selector functions missing')
|
||||
namespace = dict(np=np, VORTEX_EVENT_OFFSETS=acquisition.VORTEX_EVENT_OFFSETS)
|
||||
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(event_path), 'exec'), namespace)
|
||||
acquisition.select_vortex_event = namespace['_select_vortex_event']
|
||||
return acquisition
|
||||
|
||||
|
||||
def schedule(scene, smoke=False):
|
||||
if scene not in ('karman', 'steady', 'taylor'):
|
||||
raise ValueError('unknown scene')
|
||||
intervals = 150 if scene == 'taylor' else 400
|
||||
release = {'karman': 4576000, 'steady': 2048000, 'taylor': 1024000}[scene]
|
||||
return dict(si=32 if smoke else SI, intervals=3 if smoke else intervals,
|
||||
field_start=0 if smoke or scene == 'taylor' else 240,
|
||||
release_clock=(64 if scene == 'taylor' else 32) if smoke else release,
|
||||
smoke=bool(smoke))
|
||||
|
||||
|
||||
def available_memory(path=Path('/proc/meminfo')):
|
||||
for line in Path(path).read_text().splitlines():
|
||||
if line.startswith('MemAvailable:'):
|
||||
return int(line.split()[1])*1024
|
||||
raise RuntimeError('MemAvailable unavailable; RAM admission cannot be verified')
|
||||
|
||||
|
||||
def admit_memory(smoke=False):
|
||||
if smoke:
|
||||
return None
|
||||
available = available_memory()
|
||||
if available < MIN_AVAILABLE:
|
||||
raise MemoryError(f'MemAvailable {available} < required {MIN_AVAILABLE}')
|
||||
return available
|
||||
|
||||
|
||||
def wall_surface(scene, role, action=None):
|
||||
if role == 'target' or role == 'zero':
|
||||
return np.zeros(3, np.float64) # exactly zero physical rotation
|
||||
if scene == 'steady':
|
||||
return STEADY_SURFACE.copy()
|
||||
action = np.asarray(action)
|
||||
if action.shape != (3,) or not np.isfinite(action).all() or (np.abs(action) > 1).any():
|
||||
raise ValueError('invalid deterministic policy action')
|
||||
return ((8. if scene == 'karman' else 4.)*action+BIAS)*U
|
||||
|
||||
|
||||
def check_structure(y, v, previous_y):
|
||||
if not np.isfinite([y, v]).all() or abs(y) > D or abs(v) > .025 or abs(y-previous_y) > .025:
|
||||
raise RuntimeError('nonfinite structure, travel > D or speed > .025; no retuning')
|
||||
|
||||
|
||||
def check_clock(stepper, release_clock, completed):
|
||||
if stepper.step_count != release_clock+completed:
|
||||
raise RuntimeError('observed solver clock differs from recorded response clock')
|
||||
|
||||
|
||||
def clean_field(fields, flags):
|
||||
"""Canonical modern bit-0 fluid mask; hard-fail before nonfluid zero fill."""
|
||||
ux, uy = np.asarray(fields['ux']), np.asarray(fields['uy'])
|
||||
rho = np.asarray(fields['rho'])
|
||||
if ux.ndim != 2 or uy.shape != ux.shape or rho.shape != ux.shape:
|
||||
raise ValueError('canonical shared (ny,nx) macroscopic arrays required')
|
||||
flags = np.asarray(flags)
|
||||
if flags.size != ux.size or flags.dtype != np.uint16:
|
||||
raise ValueError('modern uint16 flags matching field required')
|
||||
flags = flags.reshape(ux.shape)
|
||||
fluid = (flags & 1) != 0
|
||||
if any(not np.isfinite(value[fluid]).all() for value in (rho, ux, uy)):
|
||||
raise RuntimeError('nonfinite FLUID macroscopic field')
|
||||
result = np.zeros((2, *ux.shape), np.float32)
|
||||
result[0, fluid], result[1, fluid] = ux[fluid], uy[fluid]
|
||||
return result, flags.copy(), fluid
|
||||
|
||||
|
||||
def select_fields(scene, plan, helpers, times, sensors, cache, event=None):
|
||||
"""Fixed-window means remain separate from independently phased diagnostics."""
|
||||
mean = helpers.accumulate_mean_fields(cache[:, 0], cache[:, 1], np.arange(len(cache)))
|
||||
result = dict(fixed_window_mean_ux=mean['mean_ux'], fixed_window_mean_uy=mean['mean_uy'])
|
||||
metadata = dict(fixed_window_count=len(cache), periodic_phase_status='not_requested')
|
||||
indices = np.array([len(cache)-1], np.int64)
|
||||
if plan['smoke']:
|
||||
metadata['selection'] = 'smoke_last_snapshot_not_physical'
|
||||
elif scene == 'taylor':
|
||||
if event is None:
|
||||
event = helpers.select_vortex_event(sensors, 'vortex_taylor')
|
||||
indices = np.asarray(event['field_indices'], np.int64)
|
||||
if (indices < 0).any() or (indices >= len(cache)).any():
|
||||
raise ValueError('authoritative event offsets exceed collected fields')
|
||||
metadata.update(selection='source_vortex_event', event=event)
|
||||
elif scene == 'karman':
|
||||
try:
|
||||
phase = helpers.assign_periodic_phase(times, sensors, minimum_crossing_interval=.5*30*SI)
|
||||
complete = helpers.complete_cycle_field_indices(times, phase['crossing_times'])
|
||||
chosen = helpers.select_phase_fields(times, phase['crossing_times'])
|
||||
cycle_mean = helpers.accumulate_mean_fields(cache[:, 0], cache[:, 1], complete['field_indices'])
|
||||
result.update(cycle_mean_ux=cycle_mean['mean_ux'], cycle_mean_uy=cycle_mean['mean_uy'])
|
||||
indices = chosen['field_indices']
|
||||
metadata.update(periodic_phase_status='available_diagnostic_only', phase_selection=chosen,
|
||||
complete_cycles=complete, crossing_times=phase['crossing_times'],
|
||||
rejected_crossing_times=phase['rejected_crossing_times'])
|
||||
except ValueError as exc:
|
||||
metadata.update(periodic_phase_status='insufficient_crossings', phase_reason=str(exc))
|
||||
metadata['selection'] = 'independent_center_probe_uy_diagnostic'
|
||||
else:
|
||||
metadata.update(selection='last_snapshot', periodic_phase_status='not_assumed_steady')
|
||||
result.update(ux=cache[indices, 0], uy=cache[indices, 1], selected_index=indices,
|
||||
selected_response_steps=times[indices])
|
||||
return result, metadata, event
|
||||
|
||||
|
||||
def add_scene(sim, scene, *, moving=False, pinball=True):
|
||||
from CelerisLab.body.motion import Translation
|
||||
if scene == 'karman':
|
||||
sim.add_body('circle', (400., 510.5), 40.)
|
||||
probes = [sim.add_body('sensor', point, 10.) for point in PROBES]
|
||||
envelope = Translation((0., 0., -D, D), .025, shared=True) if moving else None
|
||||
bodies = [sim.add_body('circle', point, 20., translation=envelope) for point in CENTRES] if pinball else []
|
||||
return probes, bodies
|
||||
|
||||
|
||||
def inject_taylor(sim):
|
||||
from CelerisLab.lbm.initializers import add_vortex
|
||||
clock = int(sim.stepper.step_count)
|
||||
with sim.ctx.activate():
|
||||
sim.stream.synchronize()
|
||||
add_vortex(sim.field, (600., 510.5), 80., .000075, 'taylor')
|
||||
sim.stream.synchronize()
|
||||
if sim.stepper.step_count != clock:
|
||||
raise RuntimeError('vortex initializer advanced response clock')
|
||||
return dict(absolute_clock=clock, response_clock=0, center=[600., 510.5], radius=80.,
|
||||
strength=.000075, type='taylor', count=1,
|
||||
operator='unmodified add_vortex: all interior reset to equilibrium; both buffers uploaded',
|
||||
impulse='full equilibrium reset discards pre-existing non-equilibrium on all interior cells')
|
||||
|
||||
|
||||
def make_release(scene, plan, cfgpath, base, out, started, cap, release_dir):
|
||||
from CelerisLab import Simulation
|
||||
if scene == 'karman' and not plan['smoke']:
|
||||
seed, force = base.saved_release(release_dir)
|
||||
return seed, force, dict(kind='verified_saved_static_release', path=release_dir,
|
||||
release_npz_sha256=sha(release_dir/'release.npz'))
|
||||
with Simulation(str(cfgpath)) as sim:
|
||||
_, bodies = add_scene(sim, scene, pinball=scene != 'taylor')
|
||||
sim.initialize()
|
||||
warm = 32 if plan['smoke'] else (512000 if scene == 'taylor' else 2048000)
|
||||
if scene == 'taylor':
|
||||
base.warmup(sim, warm, started, cap)
|
||||
first = sim.bodies.count
|
||||
for point in CENTRES:
|
||||
sim.add_body('circle', point, 20.) # runtime additions return staged ID -1
|
||||
sim.sync_bodies()
|
||||
bodies = list(range(first, first+len(CENTRES)))
|
||||
base.warmup(sim, warm-1, started, cap)
|
||||
sim.run(1, zero_obs=True, sync_obs=True)
|
||||
force = sum(float(sim.read_force(body)[1]) for body in bodies)
|
||||
seed = base.capture_seed(sim)
|
||||
if seed[3] != plan['release_clock'] or not np.isfinite(force):
|
||||
raise RuntimeError('unexpected static release clock/force')
|
||||
np.savez_compressed(out/'release.npz', current=seed[0], spare=seed[1], flags=seed[2],
|
||||
clock=seed[3], force=force)
|
||||
return seed, force, dict(kind='fresh_static_full_two_buffer_release',
|
||||
release_npz_sha256=sha(out/'release.npz'))
|
||||
|
||||
|
||||
def compress_native(path, rows, completed):
|
||||
"""Bounded disk memmap; preserve raw file until parent elects removal."""
|
||||
rows.flush()
|
||||
compressed = path.with_suffix('.npz')
|
||||
np.savez_compressed(compressed, rows=rows[:completed])
|
||||
with np.load(compressed) as data:
|
||||
retained = data['rows']
|
||||
if retained.shape != (completed, len(NATIVE_COLUMNS)):
|
||||
raise RuntimeError('compressed native shape mismatch')
|
||||
for start in range(0, completed, 10000):
|
||||
end = min(start+10000, completed)
|
||||
if not np.array_equal(retained[start:end], rows[start:end]):
|
||||
raise RuntimeError('compressed native verification failed')
|
||||
return dict(raw_path=path.name, compressed_path=compressed.name, verified=True,
|
||||
compressed_sha256=sha(compressed), raw_preserved_for_parent_cleanup=True)
|
||||
|
||||
|
||||
def run_role(scene, role, plan, cfgpath, base, helpers, metrics, seed, initial_force,
|
||||
model, norm, out, started, cap, event):
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.body.motion import MovingTables
|
||||
directory = out/role
|
||||
directory.mkdir()
|
||||
steps = plan['si']*plan['intervals']
|
||||
rows = np.lib.format.open_memmap(directory/'native.npy', mode='w+', dtype=np.float64,
|
||||
shape=(steps, len(NATIVE_COLUMNS)))
|
||||
signals = {key: np.zeros((plan['intervals'], width), np.float32) for key, width in
|
||||
[('sensor_area_mean', 6), ('sensors', 6), ('forces', 6), ('actions', 3),
|
||||
('observations', 12), ('policy_input', 12), ('raw_observation', 12)]}
|
||||
structure = base.Structure(4., 2.5e-5, 0., D)
|
||||
columns = {name: i for i, name in enumerate(MovingTables.DIAG)}
|
||||
completed = completed_si = 0
|
||||
duration = steps*U/D
|
||||
report = dict(status='running', role=role, target_is_free_body=False if role == 'target' else None,
|
||||
columns=NATIVE_COLUMNS,
|
||||
motion_window=dict(start_tU_D=max(0., duration-80.), end_tU_D=duration,
|
||||
interpretation='smoke_short_not_physical' if plan['smoke'] else
|
||||
('entire_60DU_event_not_settled' if scene == 'taylor' else 'declared_last_80DU')),
|
||||
structure=None if role == 'target' else dict(mass=structure.mass, fn=structure.frequency,
|
||||
mass_ratio=4., zeta=0., y0=0., v0=0.))
|
||||
cache, common_fluid, selected_flags, roi_cache = None, None, {}, None
|
||||
obs, force = np.zeros(12, np.float32), initial_force
|
||||
try:
|
||||
report['MemAvailable_admission_bytes'] = admit_memory(plan['smoke'])
|
||||
with Simulation(str(cfgpath)) as sim:
|
||||
probes, bodies = add_scene(sim, scene, moving=role != 'target', pinball=role != 'target')
|
||||
sim.initialize()
|
||||
if role == 'target':
|
||||
base.warmup(sim, plan['release_clock'], started, cap)
|
||||
report['initialization'] = 'own no-pinball initialization at matching absolute clock'
|
||||
else:
|
||||
report['release_transfer'] = base.install_seed(sim, seed)
|
||||
if sim.stepper.step_count != plan['release_clock']:
|
||||
raise RuntimeError('role release clocks differ')
|
||||
if scene == 'taylor':
|
||||
report['injection'] = inject_taylor(sim)
|
||||
count = plan['intervals']-plan['field_start']
|
||||
shape = (sim.field.ny, sim.field.nx)
|
||||
cache = np.empty((count, 2, *shape), np.float32)
|
||||
common_fluid = np.ones(shape, bool)
|
||||
roi = metrics.roi_slices(shape, D=D, xs=1600., y0=510.5,
|
||||
downstream_solid_extent=1272.)
|
||||
# Only compact ROI histories survive a role; full-grid histories never reach disk.
|
||||
roi_shape = cache[(slice(None), slice(None), *roi)].shape
|
||||
roi_cache = np.empty(roi_shape, np.float32)
|
||||
retain = set(event['field_indices'].tolist()) if event is not None else set()
|
||||
if scene == 'taylor' and event is None and not plan['smoke']:
|
||||
# Authoritative selector searches 38..87; any imported offset can be selected.
|
||||
retain = {i+int(offset) for i in range(38, 88) for offset in helpers.VORTEX_EVENT_OFFSETS}
|
||||
# K phase unknown until the final signal: retain flags for every boundary in RAM.
|
||||
flag_cache = np.empty((count, *shape), np.uint16) if scene == 'karman' else None
|
||||
for interval in range(plan['intervals']):
|
||||
base.check_time(started, cap)
|
||||
action = np.zeros(3, np.float32)
|
||||
signals['policy_input'][interval] = obs
|
||||
if role == 'controlled' and scene != 'steady':
|
||||
action = np.asarray(model.predict(obs, deterministic=True)[0], np.float32)
|
||||
target, effective = wall_surface(scene, role, action), np.zeros(3)
|
||||
sensor_sum, force_sum = np.zeros(6), np.zeros(6)
|
||||
for native in range(plan['si']):
|
||||
if native % 1000 == 0:
|
||||
base.check_time(started, cap)
|
||||
y, v = (0., 0.) if role == 'target' else structure.candidate(force)
|
||||
check_structure(y, v, structure.y)
|
||||
effective = .9**.25*effective + (1.-.9**.25)*target
|
||||
omega = effective/20.
|
||||
for body, w in zip(bodies, omega):
|
||||
sim.set_body(body, omega=float(w), offset=(0., y), vy=y-structure.y)
|
||||
sim.run(1, zero_obs=True, sync_obs=True)
|
||||
if bodies:
|
||||
diag = sim.moving_diagnostics()
|
||||
cme, wen = diag[:, columns['cme_y']], diag[:, columns['wen_y']]
|
||||
node = diag[:, columns['cover_jy']]-diag[:, columns['uncover_jy']]
|
||||
counters = [diag[:, columns[key]].sum() for key in ('eq_only', 'no_donor', 'crossings')]
|
||||
packed = np.array([value for body in bodies for value in sim.read_force(body)[:2]])
|
||||
else:
|
||||
cme, wen, node, counters, packed = np.zeros(3), np.zeros(3), np.zeros(3), [0., 0., 0.], np.zeros(6)
|
||||
sensors = np.array([value for probe in probes for value in sim.read_sensor(probe)[:2]])
|
||||
row = np.array([completed+1, y, v, *cme, *wen, *node, *counters, *omega])
|
||||
if not np.isfinite(row).all() or counters[1] != 0 or not np.isfinite([packed, sensors]).all():
|
||||
raise RuntimeError('nonfinite native diagnostics/observations or no_donor')
|
||||
rows[completed] = row
|
||||
completed += 1
|
||||
structure.y, structure.v = y, v
|
||||
force = float(cme.sum(dtype=np.float64)+wen.sum(dtype=np.float64))
|
||||
sensor_sum += sensors
|
||||
force_sum += packed
|
||||
check_clock(sim.stepper, plan['release_clock'], completed)
|
||||
sensors, packed = sensor_sum/plan['si'], force_sum/plan['si']
|
||||
raw = np.r_[sensors*156., packed*2.].astype(np.float32)
|
||||
if norm is not None:
|
||||
raw, obs, clipped = base.observation(sensors, packed, 2., norm)
|
||||
report['clipped_channels'] = report.get('clipped_channels', 0)+clipped
|
||||
for key, value in [('sensor_area_mean', sensors), ('sensors', sensors*156.),
|
||||
('forces', packed*2.), ('actions', action), ('observations', obs),
|
||||
('raw_observation', raw)]:
|
||||
signals[key][interval] = value
|
||||
if interval >= plan['field_start']:
|
||||
index = interval-plan['field_start']
|
||||
clean, flags, fluid = clean_field(sim.get_macroscopic(), sim.get_flags())
|
||||
if not fluid[roi].all():
|
||||
raise RuntimeError('prescribed downstream ROI contains nonfluid cells')
|
||||
cache[index] = clean
|
||||
common_fluid &= fluid
|
||||
roi_cache[index] = clean[(slice(None), *roi)]
|
||||
if flag_cache is not None:
|
||||
flag_cache[index] = flags
|
||||
elif index in retain or index == count-1:
|
||||
selected_flags[index] = flags
|
||||
completed_si += 1
|
||||
if completed_si % 10 == 0 or completed_si == plan['intervals']:
|
||||
rows.flush()
|
||||
print(json.dumps(dict(event='progress', scene=scene, role=role, interval=completed_si,
|
||||
absolute_clock=sim.stepper.step_count, y_D=y/D)), flush=True)
|
||||
times = np.arange(plan['field_start']+1, plan['intervals']+1, dtype=np.int64)*plan['si']
|
||||
selected, selection, event = select_fields(scene, plan, helpers, times,
|
||||
signals['sensors'][plan['field_start']:], cache, event)
|
||||
indices = selected['selected_index']
|
||||
selected['flags'] = flag_cache[indices] if flag_cache is not None else np.stack([selected_flags[int(i)] for i in indices])
|
||||
selected['common_fluid_all_boundaries'] = common_fluid
|
||||
selected['absolute_steps'] = selected['selected_response_steps']+plan['release_clock']
|
||||
np.savez_compressed(directory/'fields.npz', **selected)
|
||||
report.update(selection=selection, field_response_steps=times,
|
||||
field_absolute_steps=times+plan['release_clock'],
|
||||
field_window_tU_D=[int(times[0])*U/D, int(times[-1])*U/D],
|
||||
status='smoke_completed_not_physical' if plan['smoke'] else 'duration_completed_not_qualified')
|
||||
roi_mask = common_fluid[roi].copy()
|
||||
report['motion_summary'] = metrics.motion_summary(rows[max(0, completed-640000):completed, 1]/D)
|
||||
return dict(roi=roi_cache, mask=roi_mask, times=times, event=event)
|
||||
except BaseException as exc:
|
||||
report['status'] = f'error: {type(exc).__name__}: {exc}'
|
||||
raise
|
||||
finally:
|
||||
report.update(completed_native_steps=completed, completed_intervals=completed_si)
|
||||
np.savez_compressed(directory/'signals.npz', response_steps=np.arange(1, completed_si+1)*plan['si'],
|
||||
absolute_steps=plan['release_clock']+np.arange(1, completed_si+1)*plan['si'],
|
||||
**{key: value[:completed_si] for key, value in signals.items()})
|
||||
if completed:
|
||||
report['native_archive'] = compress_native(directory/'native.npy', rows, completed)
|
||||
write_json(directory/'report.json', report)
|
||||
del cache, rows
|
||||
gc.collect()
|
||||
|
||||
|
||||
def score_roles(roles, metrics):
|
||||
target = roles['target']
|
||||
common = np.logical_and.reduce([role['mask'] for role in roles.values()])
|
||||
if not common.all():
|
||||
raise ValueError('prescribed downstream ROI must be entirely common fluid')
|
||||
result = dict(definition='sqrt(mean common ROI (dux^2+duy^2)/U^2)',
|
||||
alignment='same absolute native clock; no lag/phase alignment',
|
||||
roi_xy_inclusive=[1360, 2160, 311, 710], support_cells=int(common.sum()),
|
||||
support_coverage_fraction=1.)
|
||||
for name in ('zero', 'controlled'):
|
||||
role = roles[name]
|
||||
if not np.array_equal(role['times'], target['times']):
|
||||
raise ValueError('unmatched response clocks')
|
||||
errors = np.array([metrics.field_error(*a, *b, U, common) for a, b in zip(role['roi'], target['roi'])])
|
||||
# FP64 reduction without duplicating entire history.
|
||||
mean = [a.mean(axis=0, dtype=np.float64) for a in (role['roi'], target['roi'])]
|
||||
result[name] = dict(same_clock_instantaneous_E=errors,
|
||||
mean_instantaneous_E=float(errors.mean()),
|
||||
E_of_fixed_window_mean=metrics.field_error(*mean[0], *mean[1], U, common))
|
||||
return result, common
|
||||
|
||||
|
||||
def source_manifest(base, helpers):
|
||||
files = [Path(__file__), ROOT/'research/fig2_materials/metrics.py', Path(base.__file__),
|
||||
ROOT/'research/steady_pinball/run.py', ROOT/'research/steady_pinball/config.json',
|
||||
Path(helpers.__file__), Path(helpers.__file__).parent/'legacy_test/acquire.py',
|
||||
Path(helpers.__file__).parent/'legacy_test/core/dtw_metrics.py']
|
||||
files += sorted((ROOT/'src/CelerisLab').rglob('*.py'))
|
||||
for suffix in ('*.cu', '*.cuh', '*.h'):
|
||||
files += sorted((ROOT/'src/CelerisLab').rglob(suffix))
|
||||
return {str(path.resolve()): sha(path) for path in files}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--scene', choices=('karman', 'steady', 'taylor'), required=True)
|
||||
ap.add_argument('--out', type=Path, required=True)
|
||||
ap.add_argument('--smoke', action='store_true', help='32-step warmups; 3 SI of 32; no physical score')
|
||||
ap.add_argument('--D', type=int, choices=(40,), default=40)
|
||||
ap.add_argument('--model', type=Path)
|
||||
ap.add_argument('--norm', type=Path)
|
||||
ap.add_argument('--release-dir', type=Path, default=K_RELEASE)
|
||||
ap.add_argument('--acquisition', type=Path, default=ACQUISITION)
|
||||
ap.add_argument('--wall-seconds', type=float, default=43200.)
|
||||
args = ap.parse_args(argv)
|
||||
if not np.isfinite(args.wall_seconds) or args.wall_seconds <= 0:
|
||||
ap.error('positive finite wall cap required')
|
||||
if args.scene == 'steady' and (args.model or args.norm):
|
||||
ap.error('steady constant control has no PPO or normalizer')
|
||||
out = args.out.resolve()
|
||||
if out == ROOT or ROOT in out.parents:
|
||||
ap.error('fresh output must be outside checkout')
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from research.karman_viv import run as base
|
||||
metrics = load_file('_fig2_metrics', ROOT/'research/fig2_materials/metrics.py')
|
||||
helpers = load_helpers(args.acquisition)
|
||||
model = norm = None
|
||||
inputs = {}
|
||||
if args.scene != 'steady':
|
||||
model_path, model_sha, norm_path, norm_sha = ASSETS[args.scene]
|
||||
model_path, norm_path = args.model or model_path, args.norm or norm_path
|
||||
if sha(model_path) != model_sha or sha(norm_path) != norm_sha:
|
||||
ap.error('model/normalizer hashes differ from frozen inputs')
|
||||
norm = json.loads(norm_path.read_text())
|
||||
inputs = dict(model_path=model_path, model_sha256=model_sha,
|
||||
norm_path=norm_path, norm_sha256=norm_sha)
|
||||
model = base.load_policy(model_path)
|
||||
sources = source_manifest(base, helpers)
|
||||
source = subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip()
|
||||
dirty = subprocess.check_output(['git', '-C', str(ROOT), 'status', '--porcelain'], text=True)
|
||||
plan = schedule(args.scene, args.smoke)
|
||||
admit_memory(args.smoke)
|
||||
out.mkdir(parents=True, exist_ok=False)
|
||||
cfgpath = out/'config.json'
|
||||
write_json(cfgpath, base.config(40))
|
||||
receipt = dict(scene=args.scene, plan=plan, D=D, U=U, nu=NU, actual_Re_D=50.,
|
||||
historical_case='karman_re100' if args.scene == 'karman' else args.scene,
|
||||
expected_baseline=BASELINE, source=source, git_dirty=bool(dirty), git_status=dirty,
|
||||
source_sha256=sources, inputs=inputs, centres=CENTRES, probes=PROBES,
|
||||
environment=dict(python=sys.version, platform=platform.platform(), numpy=np.__version__,
|
||||
conda_env=os.environ.get('CONDA_DEFAULT_ENV'),
|
||||
cuda_visible_devices=os.environ.get('CUDA_VISIBLE_DEVICES')),
|
||||
acquisition_event_offsets=list(helpers.VORTEX_EVENT_OFFSETS),
|
||||
event_selector='source _select_vortex_event AST verbatim; actual acquisition offsets',
|
||||
structure=dict(mass_ratio=4., fn_native=2.5e-5, fn_D_U=.2, zeta=0., travel_D=1., speed=.025),
|
||||
rhs='previous native-step sum CME_y+Wen_y; initial force last static native step',
|
||||
policy='SI mean packed CME *2; area-mean sensors *156; frozen norm; first input zeros',
|
||||
control='steady constant (0,-5.1,5.1)*U; K scale8 or Taylor scale4, bias(0,-4,4)*U',
|
||||
ema=dict(alpha=.9**.25, reset='every SI'),
|
||||
storage='one full-grid float32 RAM cache at a time; compact ROI histories in RAM; no full history field disk',
|
||||
limitation='Modern moving-plant experiment; no Legacy numerical equivalence or steady/convergence claim')
|
||||
write_json(out/'case.json', receipt)
|
||||
started, status = time.monotonic(), 'running'
|
||||
try:
|
||||
seed, force, release = make_release(args.scene, plan, cfgpath, base, out, started,
|
||||
args.wall_seconds, args.release_dir)
|
||||
receipt['release'] = release
|
||||
receipt['release'].update(clock=int(seed[3]), initial_force=force,
|
||||
current_sha256=base.array_sha(seed[0]),
|
||||
spare_sha256=base.array_sha(seed[1]), flags_sha256=base.array_sha(seed[2]))
|
||||
write_json(out/'case.json', receipt)
|
||||
roles, event = {}, None
|
||||
order = ('controlled', 'zero', 'target') if args.scene == 'taylor' else ('target', 'zero', 'controlled')
|
||||
for role in order:
|
||||
roles[role] = run_role(args.scene, role, plan, cfgpath, base, helpers, metrics,
|
||||
seed, force, model, norm, out, started, args.wall_seconds, event)
|
||||
if args.scene == 'taylor':
|
||||
event = roles[role]['event']
|
||||
if not args.smoke:
|
||||
scores, support = score_roles(roles, metrics)
|
||||
write_json(out/'same_clock_errors.json', scores)
|
||||
np.savez_compressed(out/'common_roi_support.npz', support=support)
|
||||
if source_manifest(base, helpers) != sources:
|
||||
raise RuntimeError('source files changed during collection')
|
||||
status = 'smoke_completed_not_physical' if args.smoke else 'three_roles_completed_not_qualified'
|
||||
except BaseException as exc:
|
||||
status = f'error: {type(exc).__name__}: {exc}'
|
||||
raise
|
||||
finally:
|
||||
write_json(out/'terminal.json', dict(status=status, elapsed_s=time.monotonic()-started,
|
||||
physical_score_claim=False if args.smoke else None))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,94 +0,0 @@
|
||||
"""Pure NumPy Fig2 primitives; no solver, phase selection, or probe DTW.
|
||||
|
||||
Adapted formula and inclusive ROI bounds from DynamisLab
|
||||
src/drl_pinball/eval/wake_l2.py::mean_field_metric, roi_slices, inspected
|
||||
2026-11-02 at HEAD 609116ec26f19766f5e9ce2b0df427d4e24faae8
|
||||
(working-tree source, not asserted clean). The explicit common-fluid/finite
|
||||
mask and motion summaries are additions. legacy_test/metrics.py::cycle_similarity
|
||||
was inspected only: its six probe channels are not a field recovery metric.
|
||||
"""
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
|
||||
def roi_slices(shape, *, D, xs, y0=None, downstream_solid_extent):
|
||||
"""Lab-fixed ROI: sensor-relative x/D=-6..14, y/D=-5..5, inclusive.
|
||||
|
||||
Equivalent to pinball-front-relative x/D=4..24 when xs=front+10D.
|
||||
Supply actual geometry, e.g. D=40, xs=1600, y0=510.5.
|
||||
"""
|
||||
ny, nx = shape
|
||||
y0 = (ny - 1) / 2 if y0 is None else float(y0)
|
||||
if D <= 0 or not all(np.isfinite(v) for v in (D, xs, y0, downstream_solid_extent)):
|
||||
raise ValueError("invalid geometry")
|
||||
x0, x1 = math.ceil(xs - 6 * D), math.floor(xs + 14 * D)
|
||||
lo, hi = math.ceil(y0 - 5 * D), math.floor(y0 + 5 * D)
|
||||
if x0 < 0 or lo < 0 or x1 >= nx or hi >= ny:
|
||||
raise ValueError("ROI outside grid")
|
||||
if x0 <= downstream_solid_extent:
|
||||
raise ValueError("ROI not downstream of solid extent")
|
||||
return slice(lo, hi + 1), slice(x0, x1 + 1)
|
||||
|
||||
|
||||
def common_fluid_roi_mask(fluid_masks, fields, roi):
|
||||
"""Intersect explicit boolean fluid masks, ROI, and finite 2-D fields.
|
||||
|
||||
Pass every compared role/component/sample to freeze a common support over
|
||||
time. Zero velocity never implies solid. Masks must share the lab grid.
|
||||
"""
|
||||
fields = [np.asarray(a, dtype=float) for a in fields]
|
||||
masks = [np.asarray(a) for a in fluid_masks]
|
||||
if not fields or not masks:
|
||||
raise ValueError("fields and explicit fluid masks required")
|
||||
shape = fields[0].shape
|
||||
if len(shape) != 2 or any(a.shape != shape for a in fields + masks):
|
||||
raise ValueError("equal-shape 2-D arrays required")
|
||||
if any(a.dtype != np.dtype(bool) for a in masks):
|
||||
raise ValueError("fluid masks must be boolean")
|
||||
support = np.zeros(shape, dtype=bool)
|
||||
ys, xs = roi
|
||||
for s, n in zip((ys, xs), shape):
|
||||
if not isinstance(s, slice) or s.step not in (None, 1) or not 0 <= (s.start or 0) < (n if s.stop is None else s.stop) <= n:
|
||||
raise ValueError("invalid ROI slices")
|
||||
support[ys, xs] = True
|
||||
for a in masks:
|
||||
support &= a
|
||||
for a in fields:
|
||||
support &= np.isfinite(a)
|
||||
if not support.any():
|
||||
raise ValueError("empty common finite fluid ROI")
|
||||
return support
|
||||
|
||||
|
||||
def field_error(rux, ruy, tux, tuy, u0, support):
|
||||
"""Eq1: sqrt(mean_support((dux**2 + duy**2)/U0**2)).
|
||||
|
||||
For E(mean field), pass arithmetic mean fields from matched windows.
|
||||
Mean instantaneous E requires separate calls per snapshot and then a mean;
|
||||
these operations are not interchangeable. No target-energy denominator.
|
||||
"""
|
||||
a = [np.asarray(v, dtype=float) for v in (rux, ruy, tux, tuy)]
|
||||
support = np.asarray(support)
|
||||
if a[0].ndim != 2 or any(v.shape != a[0].shape for v in a) or support.shape != a[0].shape or support.dtype != np.dtype(bool):
|
||||
raise ValueError("equal-shape 2-D fields and boolean support required")
|
||||
if not np.isfinite(u0) or u0 <= 0 or not support.any():
|
||||
raise ValueError("positive finite U0 and nonempty support required")
|
||||
if any(not np.isfinite(v[support]).all() for v in a):
|
||||
raise ValueError("nonfinite field on fixed support")
|
||||
du = (a[0][support] - a[2][support]) / u0
|
||||
dv = (a[1][support] - a[3][support]) / u0
|
||||
return float(np.sqrt(np.mean(du * du + dv * dv)))
|
||||
|
||||
|
||||
def motion_summary(displacement):
|
||||
"""Arithmetic mean, centered RMS, half peak-to-peak along time axis 0.
|
||||
|
||||
Input is displacement in caller-declared units; no implicit D scaling.
|
||||
Use the same retained window as field evaluation. No sinusoid assumption.
|
||||
"""
|
||||
a = np.asarray(displacement, dtype=float)
|
||||
if a.ndim == 0 or a.shape[0] == 0 or not np.isfinite(a).all():
|
||||
raise ValueError("nonempty finite time samples required")
|
||||
mean = a.mean(axis=0)
|
||||
return {"mean": mean, "centered_rms": np.sqrt(np.mean((a - mean) ** 2, axis=0)),
|
||||
"half_range": (a.max(axis=0) - a.min(axis=0)) / 2}
|
||||
@@ -1,332 +0,0 @@
|
||||
"""One paired zero/PPO Karman pinball VIV experiment; see docs/karman_viv.md.
|
||||
|
||||
Research-only adapter: static precursor -> two fresh, identical moving plants.
|
||||
No moving checkpoint/rollback, policy retraining, or force filtering.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from research.steady_pinball.run import Structure, config, scaled_points, CENTRES, PROBES, COLUMNS
|
||||
|
||||
BIAS = np.array([0., -4., 4.])
|
||||
MODEL_SHA = '148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d'
|
||||
NORM_SHA = '534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0'
|
||||
RELEASE_SOURCE = '516975ef6482f983ace347cb10de7450d2fbd680'
|
||||
STEP_COLUMNS = (*COLUMNS, 'omega_F', 'omega_U', 'omega_L')
|
||||
|
||||
|
||||
def sha(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, 'rb') as f:
|
||||
for block in iter(lambda: f.read(1024*1024), b''):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def array_sha(a):
|
||||
return hashlib.sha256(memoryview(np.ascontiguousarray(a)).cast('B')).hexdigest()
|
||||
|
||||
|
||||
def observation(sensor_mean, force_mean, scale, norm):
|
||||
"""Fine-grid area means/CME -> trained D20 sensor sums/forces -> frozen norm."""
|
||||
sensors = np.asarray(sensor_mean, np.float64)*scale*78.
|
||||
forces = np.asarray(force_mean, np.float64)*scale
|
||||
raw = np.asarray([*sensors, *forces], np.float32)
|
||||
preclip = np.hstack((raw[6:]/norm['force_norm_fact'],
|
||||
(raw[:6]-np.asarray(norm['sens_deviation'], np.float32)) /
|
||||
np.asarray(norm['sens_norm_fact'], np.float32)))
|
||||
if not np.isfinite(preclip).all():
|
||||
raise RuntimeError('nonfinite policy observation')
|
||||
return raw, np.clip(preclip, -1, 1).astype(np.float32), int((np.abs(preclip)>1).sum())
|
||||
|
||||
|
||||
def wall_target(action, scale):
|
||||
return (8.*np.asarray(action)+BIAS)*(.01/scale)
|
||||
|
||||
|
||||
def ema(eff, target, scale):
|
||||
alpha = .9**(1./scale**2)
|
||||
return alpha*eff + (1.-alpha)*target
|
||||
|
||||
|
||||
def load_policy(model):
|
||||
# Same inference spaces as legacy_test.core.model_loader, without importing it.
|
||||
import gymnasium as gym
|
||||
from stable_baselines3 import PPO
|
||||
class InferenceEnv(gym.Env):
|
||||
observation_space = gym.spaces.Box(-1., 1., (12,), np.float32)
|
||||
action_space = gym.spaces.Box(-1., 1., (3,), np.float32)
|
||||
def reset(self, *, seed=None, options=None):
|
||||
super().reset(seed=seed)
|
||||
return np.zeros(12, np.float32), {}
|
||||
def step(self, action):
|
||||
return np.zeros(12, np.float32), 0., False, False, {}
|
||||
return PPO.load(str(model), env=InferenceEnv(), device='cpu')
|
||||
|
||||
|
||||
def add_scene(sim, scale, moving=False, pinball=True):
|
||||
from CelerisLab.body.motion import Translation
|
||||
sim.add_body('circle', scaled_points(((200., 255.5),), scale)[0], 20.*scale)
|
||||
probes = [sim.add_body('sensor', c, 5.*scale) for c in scaled_points(PROBES, scale)]
|
||||
bodies = []
|
||||
if pinball:
|
||||
envelope = Translation((0., 0., -20.*scale, 20.*scale), .05/scale, shared=True) if moving else None
|
||||
bodies = [sim.add_body('circle', c, 10.*scale, translation=envelope)
|
||||
for c in scaled_points(CENTRES, scale)]
|
||||
return probes, bodies
|
||||
|
||||
|
||||
def capture_seed(sim):
|
||||
"""Capture both FP32 buffers at fixed geometry; never a moving-state checkpoint."""
|
||||
import pycuda.driver as cuda
|
||||
with sim.ctx.activate():
|
||||
sim.stream.synchronize()
|
||||
buffers = [np.empty_like(sim.field.ddf) for _ in range(2)]
|
||||
for dst, src in zip(buffers, (sim.field.ddf_gpu, sim.field.temp_gpu)):
|
||||
cuda.memcpy_dtoh(dst, src)
|
||||
return (*buffers, sim.get_flags(), sim.stepper.step_count)
|
||||
|
||||
|
||||
def install_seed(sim, seed):
|
||||
"""Only a new, zero-offset instance may receive the static release state."""
|
||||
import pycuda.driver as cuda
|
||||
current, spare, flags, clock = seed
|
||||
if sim.stepper.step_count != 0 or any(a.shape != sim.field.ddf.shape or a.dtype != sim.field.ddf.dtype
|
||||
for a in (current, spare)):
|
||||
raise ValueError('release transfer requires a fresh matching field')
|
||||
if not np.array_equal(sim.get_flags(), flags):
|
||||
raise ValueError('static/moving release flags differ')
|
||||
for obj in sim.bodies.objects:
|
||||
if obj.motion is not None and (obj.motion.applied != (0., 0.) or obj.motion.pending is not None):
|
||||
raise ValueError('release transfer requires zero geometry and no target')
|
||||
with sim.ctx.activate():
|
||||
sim.stream.synchronize()
|
||||
cuda.memcpy_htod(sim.field.ddf_gpu, current)
|
||||
cuda.memcpy_htod(sim.field.temp_gpu, spare)
|
||||
sim.stepper._step_count = clock # one-time research initialization, not restore()
|
||||
sim.field.invalidate_host_ddf_cache()
|
||||
copied = capture_seed(sim)
|
||||
if copied[3] != clock or any(not np.array_equal(a, b, equal_nan=True) for a, b in zip(copied[:3], seed[:3])):
|
||||
raise RuntimeError('release transfer did not preserve both buffers/flags')
|
||||
return dict(current=array_sha(copied[0]), spare=array_sha(copied[1]), flags=array_sha(copied[2]), clock=copied[3])
|
||||
|
||||
|
||||
def check_time(start, cap):
|
||||
if time.monotonic()-start > cap:
|
||||
raise TimeoutError('paired experiment wall cap')
|
||||
|
||||
|
||||
def warmup(sim, count, start, cap):
|
||||
while count:
|
||||
check_time(start, cap)
|
||||
n = min(count, 10000)
|
||||
sim.run(n)
|
||||
count -= n
|
||||
|
||||
|
||||
def precursor(cfgpath, scale, warm, si, targets, out, start, cap):
|
||||
from CelerisLab import Simulation
|
||||
with Simulation(str(cfgpath)) as sim:
|
||||
probes, _ = add_scene(sim, scale, pinball=False)
|
||||
sim.initialize()
|
||||
warmup(sim, warm, start, cap)
|
||||
target = []
|
||||
for _ in range(targets):
|
||||
check_time(start, cap)
|
||||
sim.run(si)
|
||||
target.append([v*scale*78. for p in probes for v in sim.read_sensor(p)[:2]])
|
||||
np.save(out/'target.npy', np.asarray(target, np.float32))
|
||||
first = sim.bodies.count
|
||||
for c in scaled_points(CENTRES, scale):
|
||||
sim.add_body('circle', c, 10.*scale)
|
||||
sim.sync_bodies()
|
||||
bodies = list(range(first, first+3))
|
||||
warmup(sim, warm-1, start, cap)
|
||||
sim.run(1)
|
||||
force = sum(float(sim.read_force(p)[1]) for p in bodies)
|
||||
seed = capture_seed(sim)
|
||||
np.savez(out/'release.npz', current=seed[0], spare=seed[1], flags=seed[2], clock=seed[3], force=force)
|
||||
print(json.dumps(dict(event='precursor_complete', clock=seed[3], force=force,
|
||||
elapsed_s=time.monotonic()-start)), flush=True)
|
||||
return seed, force
|
||||
|
||||
|
||||
def run_arm(name, cfgpath, scale, si, intervals, seed, initial_force, model, norm, out, start, cap, *, mass_ratio=2.):
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.body.motion import MovingTables
|
||||
cols = {k:i for i, k in enumerate(MovingTables.DIAG)}
|
||||
diameter = 20.*scale
|
||||
structure = Structure(mass_ratio, 1.e-4/scale**2, 0., diameter)
|
||||
arm = out/name
|
||||
arm.mkdir()
|
||||
rows = np.lib.format.open_memmap(arm/'steps.npy', mode='w+', dtype=np.float64,
|
||||
shape=(si*intervals, len(STEP_COLUMNS)))
|
||||
rows[:] = np.nan
|
||||
signals = {k:np.full((intervals, width), np.nan, np.float32) for k, width in
|
||||
[('sensors',6), ('forces',6), ('actions',3), ('observations',12)]}
|
||||
completed = completed_intervals = clipped = 0
|
||||
status, transfer = 'running', None
|
||||
force = initial_force
|
||||
obs = np.zeros(12, np.float32)
|
||||
arm_start = time.monotonic()
|
||||
try:
|
||||
with Simulation(str(cfgpath)) as sim:
|
||||
probes, bodies = add_scene(sim, scale, moving=True)
|
||||
sim.initialize()
|
||||
transfer = install_seed(sim, seed)
|
||||
for i in range(intervals):
|
||||
action = -BIAS/8 if name == 'zero' else np.asarray(model.predict(obs, deterministic=True)[0], np.float32)
|
||||
if action.shape != (3,) or not np.isfinite(action).all() or (np.abs(action)>1).any():
|
||||
raise RuntimeError('invalid policy action')
|
||||
target, eff = wall_target(action, scale), np.zeros(3)
|
||||
sensor_sum, force_sum = np.zeros(6), np.zeros(6)
|
||||
for k in range(si):
|
||||
if k % 1000 == 0:
|
||||
check_time(start, cap)
|
||||
y1, v1 = structure.candidate(force)
|
||||
if not np.isfinite([y1,v1]).all() or abs(y1)>diameter:
|
||||
raise RuntimeError('nonfinite structure or |y| > D; no retuning')
|
||||
eff = ema(eff, target, scale)
|
||||
omega = eff/(diameter/2)
|
||||
for p, w in zip(bodies, omega):
|
||||
sim.set_body(p, omega=float(w), offset=(0., y1), vy=y1-structure.y)
|
||||
sim.run(1, zero_obs=True, sync_obs=True)
|
||||
d = sim.moving_diagnostics()
|
||||
cme, wen = d[:,cols['cme_y']], d[:,cols['wen_y']]
|
||||
node = d[:,cols['cover_jy']]-d[:,cols['uncover_jy']]
|
||||
row = (completed+1,y1,v1,*cme,*wen,*node,
|
||||
d[:,cols['eq_only']].sum(),d[:,cols['no_donor']].sum(),d[:,cols['crossings']].sum(),*omega)
|
||||
if not np.isfinite(row).all() or row[13] != 0:
|
||||
raise RuntimeError('nonfinite moving diagnostic or no_donor')
|
||||
f = np.array([v for p in bodies for v in sim.read_force(p)[:2]])
|
||||
s = np.array([v for p in probes for v in sim.read_sensor(p)[:2]])
|
||||
if not np.isfinite([f,s]).all():
|
||||
raise RuntimeError('nonfinite force/sensor observation')
|
||||
# Policy sees packed CME as in fixed-body training; structure sees CME+Wen.
|
||||
force_sum += f
|
||||
sensor_sum += s
|
||||
force = float(cme.sum(dtype=np.float64)+wen.sum(dtype=np.float64))
|
||||
rows[completed] = row
|
||||
completed += 1
|
||||
structure.y, structure.v = y1,v1
|
||||
raw, obs, nclip = observation(sensor_sum/si, force_sum/si, scale, norm)
|
||||
clipped += nclip
|
||||
for key, val in [('sensors',raw[:6]),('forces',raw[6:]),('actions',action),('observations',obs)]:
|
||||
signals[key][i] = val
|
||||
completed_intervals = i+1
|
||||
if (i+1)%10 == 0 or i+1 == intervals:
|
||||
rows.flush()
|
||||
np.savez(arm/'signals.npz', **signals)
|
||||
print(json.dumps(dict(event='progress', arm=name, interval=i+1, step=completed,
|
||||
tU_D=completed*.01/scale/diameter, y_D=y1/diameter,
|
||||
elapsed_s=time.monotonic()-arm_start)), flush=True)
|
||||
status = 'duration_completed_not_qualified'
|
||||
except BaseException as exc:
|
||||
status = f'error: {type(exc).__name__}: {exc}'
|
||||
raise
|
||||
finally:
|
||||
rows.flush()
|
||||
np.savez(arm/'signals.npz', **signals)
|
||||
report = dict(status=status, completed_steps=completed, completed_intervals=completed_intervals,
|
||||
elapsed_s=time.monotonic()-arm_start, release_transfer=transfer, clipped_observation_channels=clipped,
|
||||
columns=STEP_COLUMNS, M=structure.mass,K=structure.stiffness,fn=structure.frequency,damping=0.,
|
||||
max_abs_y_D=float(np.max(np.abs(rows[:completed,1]))/diameter) if completed else None)
|
||||
(arm/'report.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
|
||||
|
||||
def saved_release(directory):
|
||||
"""Read the retained m*=2 static release; not a moving-state restart."""
|
||||
case = json.loads((directory/'case.json').read_text())
|
||||
expected = dict(source=RELEASE_SOURCE, model_sha256=MODEL_SHA, norm_sha256=NORM_SHA,
|
||||
smoke=False, D=40, U=.005, nu=.004, warmup_each=2048000,
|
||||
target_intervals=150, SI=3200)
|
||||
if any(case.get(k) != v for k, v in expected.items()):
|
||||
raise ValueError('saved release case does not match the frozen precursor')
|
||||
if json.loads((directory/'config.json').read_text()) != config(40):
|
||||
raise ValueError('saved release config differs')
|
||||
with np.load(directory/'release.npz') as data:
|
||||
seed = (data['current'], data['spare'], data['flags'], int(data['clock']))
|
||||
force = float(data['force'])
|
||||
transfer = json.loads((directory/'zero/report.json').read_text())['release_transfer']
|
||||
if seed[3] != transfer['clock'] or seed[3] != 4576000 or not np.isfinite(force):
|
||||
raise ValueError('saved release clock/force differs')
|
||||
if [array_sha(a) for a in seed[:3]] != [transfer[k] for k in ('current','spare','flags')]:
|
||||
raise ValueError('saved release buffer/flags hash differs')
|
||||
target = np.load(directory/'target.npy')
|
||||
if target.shape != (150,6) or not np.isfinite(target).all():
|
||||
raise ValueError('saved target is incomplete or nonfinite')
|
||||
return seed, force
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--model', type=Path, required=True)
|
||||
ap.add_argument('--norm', type=Path, required=True)
|
||||
ap.add_argument('--out', type=Path, required=True)
|
||||
ap.add_argument('--smoke', action='store_true', help='short plumbing only; no physical conclusion')
|
||||
ap.add_argument('--mass-ratio', type=float, choices=(2.,4.), default=2.,
|
||||
help='4 is the single declared exploratory candidate; fn and damping unchanged')
|
||||
ap.add_argument('--release-dir', type=Path, help='retained m*=2 precursor; skip warmup')
|
||||
ap.add_argument('--wall-seconds', type=float, default=14400.)
|
||||
args = ap.parse_args()
|
||||
if not np.isfinite(args.wall_seconds) or args.wall_seconds<=0:
|
||||
ap.error('positive finite wall cap required')
|
||||
if args.release_dir is not None and args.smoke:
|
||||
ap.error('saved physical release cannot be used for the plumbing smoke')
|
||||
if sha(args.model)!=MODEL_SHA or sha(args.norm)!=NORM_SHA:
|
||||
ap.error('model/normalizer must match frozen provenance')
|
||||
out = args.out.resolve()
|
||||
if out == ROOT or ROOT in out.parents:
|
||||
ap.error('output must be outside checkout')
|
||||
out.mkdir(parents=True, exist_ok=False)
|
||||
norm = json.loads(args.norm.read_text())
|
||||
model = load_policy(args.model)
|
||||
scale = 2.
|
||||
warm, si, targets, intervals = (32,8,2,4) if args.smoke else (2048000,3200,150,400)
|
||||
cfgpath = out/'config.json'
|
||||
cfgpath.write_text(json.dumps(config(40),indent=2)+'\n')
|
||||
receipt = dict(source=subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True).strip(),
|
||||
model_sha256=sha(args.model),norm_sha256=sha(args.norm),smoke=args.smoke,D=40,U=.005,nu=.004,
|
||||
warmup_each=warm,target_intervals=targets,SI=si,response_intervals=intervals,
|
||||
response_steps=si*intervals,response_tU_D=si*intervals*.005/40,
|
||||
rhs='previous native step sum CME_y+Wen_y; policy uses interval packed CME',
|
||||
probe_frame='lab-fixed; fine area mean * 2 * trained 78 cells',
|
||||
ema_native_alpha=.9**.25, structural_damping=0., mass_ratio=args.mass_ratio,
|
||||
exploratory=args.mass_ratio==4., columns=STEP_COLUMNS)
|
||||
loaded = None
|
||||
if args.release_dir is not None:
|
||||
loaded = saved_release(args.release_dir)
|
||||
cfgpath.write_bytes((args.release_dir/'config.json').read_bytes())
|
||||
(out/'target.npy').write_bytes((args.release_dir/'target.npy').read_bytes())
|
||||
receipt.update(release_dir=str(args.release_dir.resolve()), release_source=RELEASE_SOURCE,
|
||||
release_sha256=sha(args.release_dir/'release.npz'),
|
||||
warmup_executed=0, target_sha256=sha(out/'target.npy'))
|
||||
(out/'case.json').write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
start = time.monotonic()
|
||||
status = 'running'
|
||||
try:
|
||||
seed, force = loaded if loaded is not None else precursor(cfgpath,scale,warm,si,targets,out,start,args.wall_seconds)
|
||||
for name in ('zero','controlled'):
|
||||
run_arm(name,cfgpath,scale,si,intervals,seed,force,model,norm,out,start,args.wall_seconds,
|
||||
mass_ratio=args.mass_ratio)
|
||||
status = 'smoke_completed' if args.smoke else 'pair_completed_not_qualified'
|
||||
except BaseException as exc:
|
||||
status = f'error: {type(exc).__name__}: {exc}'
|
||||
raise
|
||||
finally:
|
||||
(out/'terminal.json').write_text(json.dumps(dict(status=status,elapsed_s=time.monotonic()-start),indent=2)+'\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"_doc": "Legacy geometry Re_D=50, U0=0.01, SI=800 baseline (kar_legacy_re50d_u001)",
|
||||
"grid": {
|
||||
"lattice_model": "D2Q9",
|
||||
"nx": 1280,
|
||||
"ny": 512,
|
||||
"nz": 1
|
||||
},
|
||||
"physics": {
|
||||
"data_type": "FP32",
|
||||
"viscosity": 0.004,
|
||||
"velocity": 0.01,
|
||||
"rho": 1.0
|
||||
},
|
||||
"method": {
|
||||
"collision": "MRT",
|
||||
"streaming": "double_buffer",
|
||||
"store_precision": "FP32",
|
||||
"ddf_shifting": false,
|
||||
"les": {
|
||||
"enabled": false,
|
||||
"cs": 0.16,
|
||||
"closed_form": true
|
||||
},
|
||||
"trt": {
|
||||
"magic_param": 0.1875
|
||||
},
|
||||
"inlet": {
|
||||
"profile": "parabolic",
|
||||
"scheme": "regularized",
|
||||
"regularized_neq_damp": 1.0
|
||||
},
|
||||
"outlet": {
|
||||
"mode": "neq_extrap",
|
||||
"backflow_clamp": true,
|
||||
"blend_alpha": 0.7,
|
||||
"srt_neq_damp": 0.5
|
||||
},
|
||||
"y_wall_bc": "bounce_back",
|
||||
"omega_guard": {
|
||||
"min": 0.01,
|
||||
"max": 1.99
|
||||
}
|
||||
},
|
||||
"cuda": {
|
||||
"threads_per_block": 256,
|
||||
"compute_capability": "auto"
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
"""Uncontrolled paper-geometry steady Re_D=50, one transverse assembly DOF.
|
||||
|
||||
Three predeclared zero-damping structures; --smoke uses only 32 + 32 steps.
|
||||
See docs/steady_pinball_fiv.md for historical solver differences and new structure.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
D, U, NU = 20., .01, .004
|
||||
CENTRES = ((600., 255.5), (626., 270.5), (626., 240.5))
|
||||
PROBES = ((800., 295.5), (800., 255.5), (800., 215.5))
|
||||
WARMUP, STEPS, PERIOD = 512000, 600000, 10000
|
||||
STRUCTURES = {'Z0': (10., 1.e-4), 'M2': (2., 1.e-4), 'R2': (2., 6.e-5)}
|
||||
COLUMNS = ('step', 'y', 'v', 'cme_F', 'cme_U', 'cme_L', 'wen_F', 'wen_U', 'wen_L',
|
||||
'node_F', 'node_U', 'node_L', 'eq_only', 'no_donor', 'crossings')
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def config(diameter=D):
|
||||
"""Diffusive refinement preserves the inlet/outlet and half-grid y walls."""
|
||||
scale = diameter / D
|
||||
if diameter not in (20, 40, 60):
|
||||
raise ValueError('diameter must be 20, 40 or 60')
|
||||
cfg = json.loads(Path(__file__).with_name('config.json').read_text())
|
||||
cfg['grid']['nx'] = round((cfg['grid']['nx'] - 1) * scale + 1)
|
||||
cfg['grid']['ny'] = round((cfg['grid']['ny'] - 2) * scale + 2)
|
||||
cfg['physics']['velocity'] = U / scale
|
||||
return cfg
|
||||
|
||||
|
||||
def scaled_points(points, scale):
|
||||
return tuple((scale*x, .5 + scale*(y-.5)) for x, y in points)
|
||||
|
||||
|
||||
class Structure:
|
||||
"""Exact damped oscillator update for a force held over dt=1; no force filter."""
|
||||
def __init__(self, mass_ratio=10., frequency=1.e-4, zeta=0., diameter=D):
|
||||
if not (math.isfinite(mass_ratio) and mass_ratio > 0. and
|
||||
math.isfinite(frequency) and frequency > 0. and
|
||||
math.isfinite(zeta) and 0. <= zeta < 1.):
|
||||
raise ValueError('positive finite mass ratio/frequency and 0 <= zeta < 1 required')
|
||||
self.mass_ratio, self.frequency = mass_ratio, frequency
|
||||
self.mass = mass_ratio * 3. * math.pi * diameter * diameter / 4.
|
||||
self.omega = 2. * math.pi * frequency
|
||||
self.zeta = zeta
|
||||
self.stiffness = self.mass * self.omega**2
|
||||
self.damping = 2. * self.zeta * self.mass * self.omega
|
||||
self.y = self.v = 0.
|
||||
a = self.zeta * self.omega
|
||||
wd = self.omega * math.sqrt(1. - self.zeta**2)
|
||||
self.ec = math.exp(-a) * math.cos(wd)
|
||||
self.es = math.exp(-a) * math.sin(wd) / wd
|
||||
self.a = a
|
||||
|
||||
def candidate(self, force):
|
||||
equilibrium = force / self.stiffness
|
||||
z = self.y - equilibrium
|
||||
return (equilibrium + (self.ec + self.a*self.es)*z + self.es*self.v,
|
||||
-self.omega**2*self.es*z + (self.ec - self.a*self.es)*self.v)
|
||||
|
||||
|
||||
def summarize(rows, diameter=D, block_steps=10*PERIOD):
|
||||
windows = []
|
||||
for start in range(0, len(rows), block_steps):
|
||||
block = rows[start:start + block_steps]
|
||||
fy = block[:, 3:9].sum(axis=1)
|
||||
windows.append(dict(start_step=int(block[0, 0]), end_step=int(block[-1, 0]),
|
||||
y_mean_D=float(block[:, 1].mean()/diameter),
|
||||
y_rms_D=float(np.sqrt(np.mean(block[:, 1]**2))/diameter),
|
||||
y_peak_D=float(np.abs(block[:, 1]).max()/diameter),
|
||||
fy_mean=float(fy.mean()), fy_rms=float(np.sqrt(np.mean(fy**2))),
|
||||
mean_power=float(np.mean(fy*block[:, 2]))))
|
||||
return windows
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--out', required=True, type=Path)
|
||||
ap.add_argument('--fixed', action='store_true', help='same modern moving-path geometry held fixed')
|
||||
ap.add_argument('--smoke', action='store_true', help='32+32 steps, no physical result')
|
||||
ap.add_argument('--case', choices=STRUCTURES, default='Z0',
|
||||
help='predeclared structure; all cases have zero structural damping')
|
||||
ap.add_argument('--D', type=int, choices=(20, 40, 60), default=20,
|
||||
help='diameter in cells; diffusive scaling, matched geometry and physical duration')
|
||||
ap.add_argument('--wall-seconds', type=float, default=7200., help='whole-case wall-clock cap')
|
||||
args = ap.parse_args()
|
||||
if not math.isfinite(args.wall_seconds) or args.wall_seconds <= 0:
|
||||
ap.error('--wall-seconds must be finite and positive')
|
||||
scale, time_scale = args.D / D, (args.D // int(D))**2
|
||||
velocity = U / scale
|
||||
centres, probes = scaled_points(CENTRES, scale), scaled_points(PROBES, scale)
|
||||
period = PERIOD*time_scale
|
||||
out = args.out.resolve()
|
||||
if out == ROOT or ROOT in out.parents:
|
||||
ap.error('output must be outside the checkout')
|
||||
out.mkdir(parents=True, exist_ok=False)
|
||||
cfg = config(args.D)
|
||||
(out/'config.json').write_text(json.dumps(cfg, indent=2)+'\n')
|
||||
source = subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip()
|
||||
mass_ratio, frequency = STRUCTURES[args.case]
|
||||
s = Structure(mass_ratio, frequency/time_scale, zeta=0., diameter=args.D)
|
||||
warmup, count = (32, 32) if args.smoke else (WARMUP*time_scale, STEPS*time_scale)
|
||||
rows = np.lib.format.open_memmap(out/'steps.npy', mode='w+', dtype=np.float64,
|
||||
shape=(count, len(COLUMNS)))
|
||||
rows[:] = np.nan
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.body.motion import MovingTables, Translation
|
||||
names = MovingTables.DIAG
|
||||
col = {k: names.index(k) for k in names}
|
||||
status, completed, f_last = 'running', 0, 0.
|
||||
started = time.monotonic()
|
||||
report = dict(source=source, case=args.case, fixed=args.fixed, smoke=args.smoke, columns=COLUMNS,
|
||||
structure=dict(mass=s.mass, stiffness=s.stiffness, damping=s.damping,
|
||||
mass_ratio_three_disks=s.mass_ratio,
|
||||
zeta=s.zeta, fn=s.frequency, Tn=1./s.frequency, y0=0., v0=0.),
|
||||
warmup_steps=warmup, requested_steps=count, dt=1., D=args.D, U=velocity, nu=NU,
|
||||
relative_to_D20=dict(dx=1/scale, dt=1/time_scale),
|
||||
centres=centres, probes=probes, wall_planes=(.5, cfg['grid']['ny']-1.5),
|
||||
wall_cap_seconds=args.wall_seconds,
|
||||
duration_Tn=count*s.frequency, duration_tU_D=count*velocity/args.D,
|
||||
summary_block_steps=10*period,
|
||||
rhs='sum of three instantaneous CME_y + Wen_y, one-step lag, no filtering')
|
||||
try:
|
||||
with Simulation(str(out/'config.json')) as sim:
|
||||
# Match Legacy object order; probes remain passive and fixed in the lab.
|
||||
for c in probes:
|
||||
sim.add_body('sensor', c, 5.*scale)
|
||||
envelope = Translation((0., 0., -args.D, args.D), .05/scale, shared=True)
|
||||
bodies = [sim.add_body('circle', c, args.D/2, translation=envelope) for c in centres]
|
||||
sim.initialize()
|
||||
# At release obtain the last instantaneous force, not a warmup average.
|
||||
remaining = warmup - 1
|
||||
while remaining:
|
||||
if time.monotonic()-started > args.wall_seconds:
|
||||
raise TimeoutError('whole-case wall cap during warmup')
|
||||
chunk = min(10000, remaining)
|
||||
sim.run(chunk)
|
||||
remaining -= chunk
|
||||
sim.run(1)
|
||||
d = sim.moving_diagnostics()
|
||||
f_last = float(d[:, col['cme_y']].sum(dtype=np.float64) + d[:, col['wen_y']].sum(dtype=np.float64))
|
||||
report['warmup_elapsed_s'] = time.monotonic()-started
|
||||
print(json.dumps(dict(event='release', **report)), flush=True)
|
||||
for n in range(count):
|
||||
if n % 1000 == 0 and time.monotonic()-started > args.wall_seconds:
|
||||
raise TimeoutError('whole-case wall cap during response')
|
||||
y1, v1 = (0., 0.) if args.fixed else s.candidate(f_last)
|
||||
if not all(map(math.isfinite, (y1, v1))) or abs(y1) > args.D:
|
||||
status = 'stopped_nonfinite_or_travel_bound'
|
||||
break
|
||||
if not args.fixed:
|
||||
for bid in bodies:
|
||||
sim.set_body(bid, offset=(0., y1), vy=y1-s.y)
|
||||
sim.run(1)
|
||||
d = sim.moving_diagnostics()
|
||||
cme, wen = d[:, col['cme_y']], d[:, col['wen_y']]
|
||||
node = d[:, col['cover_jy']] - d[:, col['uncover_jy']]
|
||||
row = (n+1, y1, v1, *cme, *wen, *node,
|
||||
d[:, col['eq_only']].sum(), d[:, col['no_donor']].sum(), d[:, col['crossings']].sum())
|
||||
if not np.isfinite(row).all() or row[-2] != 0:
|
||||
status = 'stopped_nonfinite_or_no_donor'
|
||||
break
|
||||
rows[n] = row
|
||||
completed = n+1
|
||||
s.y, s.v = y1, v1
|
||||
f_last = float(cme.sum(dtype=np.float64)+wen.sum(dtype=np.float64))
|
||||
if completed % period == 0:
|
||||
rows.flush()
|
||||
print(json.dumps(dict(event='progress', step=completed, t_Tn=completed*s.frequency,
|
||||
y_D=s.y/args.D, elapsed_s=time.monotonic()-started)), flush=True)
|
||||
else:
|
||||
status = 'smoke_completed' if args.smoke else 'duration_completed_not_qualified'
|
||||
except BaseException as exc:
|
||||
status = f'error: {type(exc).__name__}: {exc}'
|
||||
raise
|
||||
finally:
|
||||
rows.flush()
|
||||
report.update(status=status, completed_steps=completed, elapsed_s=time.monotonic()-started,
|
||||
windows=summarize(rows[:completed], args.D, 10*period),
|
||||
totals={key: float(rows[:completed, i].sum()) for key, i in
|
||||
[('eq_only', 12), ('no_donor', 13), ('crossings', 14)]},
|
||||
limitation='Uncontrolled development response only. No automatic settling, grid convergence, control or historical-solver equivalence claim.')
|
||||
(out/'report.json').write_text(json.dumps(report, indent=2)+'\n')
|
||||
print(json.dumps(report), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,23 +0,0 @@
|
||||
# Research VIV driver — coupling contract (not production)
|
||||
|
||||
`run.py` couples a 1-DOF undamped spring–mass (`viv.py`) to the production
|
||||
moving-circle API. Everything specific to fluid–structure coupling lives here,
|
||||
not in `src/`.
|
||||
|
||||
- Per step: known (y_n, v_n) and the last completed force F_last → exact
|
||||
harmonic candidate (y_{n+1}, v_{n+1}) → `set_body(offset=(0, y_{n+1}),
|
||||
vy=y_{n+1} − y_n)` (interval wall velocity, lattice dt = 1) → `run(1)` →
|
||||
commit y, v and F_last only after the step succeeded. One step of explicit
|
||||
force lag (weak coupling); no sub-iteration.
|
||||
- Structural RHS = CME + Wen (y), read from `moving_diagnostics()` by the
|
||||
names in `MovingTables.DIAG`; raw CME, the `obs` force and node terms are
|
||||
logged as diagnostics. This is a stage choice, not a validated force law.
|
||||
- Production guarantees used: fail-closed target commit (synchronized step,
|
||||
`no_donor == 0`), input rejection before mutation, a failed device step
|
||||
marks the Simulation failed.
|
||||
- Not provided, by design: device-resident structure state or batched
|
||||
(multi-step) coupling, strong/sub-iterated coupling (would need state
|
||||
rollback), more than one DOF. These are future work and would be a separate
|
||||
execution path, not an abstraction over this driver.
|
||||
- Recorded result and limits: `docs/validation/moving_bodies_stage1.md`;
|
||||
predeclaration: `docs/validation/viv_re150_predeclare.md`.
|
||||
@@ -1,136 +0,0 @@
|
||||
"""One-shot predeclared VIV run (docs/validation/viv_re150_predeclare.md).
|
||||
|
||||
Re 150, U* = 5, m/(rho D^2) = 2, zeta = 0, D = 40 on a nominal 75 D x 60 D
|
||||
lattice (uniform inlet, NEQ outlet, free-slip y walls). The cylinder is at
|
||||
(30 D, (ny - 1)/2); y-wall planes are .5 and ny - 1.5 in lattice coordinates.
|
||||
Structural RHS = CME + Wen (diagnostics: raw CME, node terms).
|
||||
Research driver only: Simulation API, no production change.
|
||||
|
||||
PYTHONPATH=src python research/viv_re150/run.py --out /path/outside/checkout
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
|
||||
import viv
|
||||
|
||||
D, U = 40, 0.04
|
||||
TEMPLATE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "src",
|
||||
"CelerisLab", "configs", "config_lbm.json")
|
||||
|
||||
|
||||
def config(D=D, U=U):
|
||||
nx, ny = 75 * D, 60 * D
|
||||
with open(TEMPLATE) as fh:
|
||||
cfg = json.load(fh)
|
||||
cfg["grid"].update(lattice_model="D2Q9", nx=nx, ny=ny, nz=1)
|
||||
cfg["physics"].update(data_type="FP32", viscosity=U * D / viv.RE, velocity=U, rho=1.0)
|
||||
cfg["method"].update(collision="MRT", streaming="double_buffer", store_precision="FP32",
|
||||
ddf_shifting=False, y_wall_bc="free_slip")
|
||||
cfg["method"]["les"]["enabled"] = False
|
||||
cfg["method"]["inlet"]["profile"] = "uniform"
|
||||
return cfg, nx, ny
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--D", type=int, default=D)
|
||||
ap.add_argument("--smoke", action="store_true",
|
||||
help="plumbing smoke only: exactly --steps steps (<= 64), never scored")
|
||||
ap.add_argument("--steps", type=int, default=0)
|
||||
args = ap.parse_args()
|
||||
if args.smoke != bool(args.steps) or args.steps > 64 or args.steps < 0:
|
||||
raise SystemExit("--smoke requires --steps N with 1 <= N <= 64 (and only with --smoke)")
|
||||
if args.D != D and not args.smoke:
|
||||
raise SystemExit("the physical case is predeclared at D = 40")
|
||||
if os.path.realpath(args.out).startswith(os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))):
|
||||
raise SystemExit("--out must be outside the checkout")
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
for name in ("report.json", "steps.npy"): # never overwrite earlier evidence
|
||||
if os.path.exists(os.path.join(args.out, name)):
|
||||
raise SystemExit(f"{args.out} already holds {name}; use a fresh output directory")
|
||||
Dn = args.D
|
||||
cfg, nx, ny = config(Dn)
|
||||
s, cyc = viv.Structure(Dn, U), viv.Cycles(Dn)
|
||||
T_n = 1.0 / s.f_n
|
||||
step_cap, n_seed = int(round(viv.STEP_CAP_TN * T_n)), int(round(viv.SEED_TN * T_n))
|
||||
quiet = int(round(5 * T_n))
|
||||
if args.smoke:
|
||||
step_cap = args.steps
|
||||
centre = (30.0 * Dn, 0.5 * (ny - 1))
|
||||
|
||||
from CelerisLab.body.motion import MovingTables, Translation
|
||||
col = {k: MovingTables.DIAG.index(k) for k in ("cme_y", "wen_y", "cover_jy", "uncover_jy", "no_donor")}
|
||||
from CelerisLab.simulation import Simulation
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
|
||||
json.dump(cfg, fh)
|
||||
sim = Simulation(fh.name)
|
||||
rows, status, seed_step, f_last = [], "running", None, 0.0
|
||||
try:
|
||||
body = sim.add_body("circle", centre, 0.5 * Dn,
|
||||
translation=Translation((0.0, 0.0, -1.5 * Dn, 1.5 * Dn), 0.5))
|
||||
sim.initialize()
|
||||
for n in range(step_cap):
|
||||
# rows[k] holds the state y_{k+1}; the declared window is y_t, t in [n_seed - 5 T_n, n_seed)
|
||||
if not args.smoke and n == n_seed and \
|
||||
max(abs(r[1]) for r in rows[n_seed - quiet - 1:n_seed - 1]) < viv.ELIGIBLE * Dn:
|
||||
s.v += s.seed_velocity(Dn) # after the quiet check, before the update
|
||||
seed_step = n
|
||||
cyc.discard_current()
|
||||
cyc.reset_proof()
|
||||
y1, v1 = s.candidate(f_last)
|
||||
if not (math.isfinite(y1) and math.isfinite(v1)) or abs(y1) > viv.Y_LIMIT * Dn:
|
||||
status = "fail_bound_or_nonfinite"
|
||||
break
|
||||
try:
|
||||
sim.set_body(body, offset=(0.0, y1), vy=y1 - s.y)
|
||||
except ValueError as exc: # input rejection: stop, no retry
|
||||
status = f"rejected: {exc}"
|
||||
break
|
||||
sim.run(1) # device failures raise and mark the Simulation failed
|
||||
d = sim.moving_diagnostics()[0]
|
||||
f_obs = sim.read_force(body)[1]
|
||||
f_rhs = float(d[col["cme_y"]] + d[col["wen_y"]]) # CME + Wen (y)
|
||||
row = (n, y1, v1, f_rhs, float(d[col["cme_y"]]), float(d[col["wen_y"]]), float(f_obs),
|
||||
float(d[col["cover_jy"]] - d[col["uncover_jy"]]), float(d[col["no_donor"]]))
|
||||
if not viv.finite(row): # every recorded output, not only the RHS
|
||||
status = "fail_nonfinite_output"
|
||||
break
|
||||
s.y, s.v, f_last = y1, v1, f_rhs # commit only after a successful step
|
||||
rows.append(row)
|
||||
if args.smoke:
|
||||
continue # no settle / score in the plumbing smoke
|
||||
if cyc.update(s.y) == "scored":
|
||||
status = "scored"
|
||||
break
|
||||
if cyc.counted >= viv.CYCLE_CAP:
|
||||
status = "timeout_response_cycles"
|
||||
break
|
||||
else:
|
||||
status = ("smoke_ok" if all(r[8] == 0.0 for r in rows) else "smoke_no_donor") \
|
||||
if args.smoke else "timeout_native_steps"
|
||||
except BaseException as exc:
|
||||
status = f"error: {exc!r}"
|
||||
raise
|
||||
finally:
|
||||
np.save(os.path.join(args.out, "steps.npy"), np.asarray(rows, float))
|
||||
report = dict(status=status, smoke=args.smoke, D=Dn, steps=len(rows), t_U_over_D=len(rows) * U / Dn, T_n=T_n,
|
||||
seed_step=seed_step, counted_cycles=cyc.counted, cycles=cyc.log,
|
||||
columns=["n", "y", "v", "F_rhs_cme_wen", "F_cme", "F_wen", "F_obs",
|
||||
"node_cover_minus_uncover_y", "no_donor"])
|
||||
if status == "scored" and not args.smoke:
|
||||
report["score"] = viv.score(cyc.window)
|
||||
with open(os.path.join(args.out, "report.json"), "w") as fh:
|
||||
json.dump(report, fh, indent=1)
|
||||
sim.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,126 +0,0 @@
|
||||
"""Pure logic of the predeclared Re 150 free transverse VIV case (stdlib only).
|
||||
|
||||
Contract: docs/validation/viv_re150_predeclare.md. Oscillator update with a
|
||||
constant force over one lattice step, response-cycle tracking with fixed y = 0
|
||||
crossings, eligibility, 10-cycle blocks, settling/scoring with deterministic
|
||||
restart, and the conditional seed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
RE, U_STAR, MASS_NUMBER = 150.0, 5.0, 2.0 # m/(rho D^2) = 2 -> m* = 8/pi
|
||||
TARGET_A, BAND = 0.520, 0.10 # Qin §4.5 maximum amplitude, ±10 %
|
||||
ELIGIBLE, BLOCK, LAST5 = 0.01, 10, 5 # in D; cycles; cycles
|
||||
WINDOW_TOL, LAST5_TOL = 0.02, 0.01
|
||||
CYCLE_CAP, STEP_CAP_TN, SEED_TN, Y_LIMIT = 150, 200, 40, 1.0
|
||||
|
||||
|
||||
def finite(values):
|
||||
"""True iff every recorded value is finite (fail-closed output check)."""
|
||||
return all(math.isfinite(float(v)) for v in values)
|
||||
|
||||
|
||||
class Structure:
|
||||
"""Undamped spring-mass, exact harmonic update over dt = 1 with constant force."""
|
||||
|
||||
def __init__(self, D, u):
|
||||
self.m = MASS_NUMBER * D * D # lattice rho = 1
|
||||
self.f_n = u / (U_STAR * D)
|
||||
self.w = 2.0 * math.pi * self.f_n
|
||||
self.k = self.m * self.w * self.w
|
||||
self.y = self.v = 0.0
|
||||
|
||||
def candidate(self, force):
|
||||
ye = force / self.k
|
||||
c, s = math.cos(self.w), math.sin(self.w)
|
||||
return (ye + (self.y - ye) * c + self.v / self.w * s,
|
||||
-(self.y - ye) * self.w * s + self.v * c)
|
||||
|
||||
def seed_velocity(self, D):
|
||||
return self.w * ELIGIBLE * D # amplitude ~ .01 D
|
||||
|
||||
|
||||
class Cycles:
|
||||
"""Upward y = 0 crossings, eligibility, blocks, settle/score with restart."""
|
||||
|
||||
def __init__(self, D):
|
||||
self.D = D
|
||||
self.reset_proof()
|
||||
self.counted = 0
|
||||
self.log = [] # one dict per completed cycle
|
||||
self._cur = None
|
||||
self._prev = 0.0
|
||||
self._since = 0
|
||||
|
||||
def reset_proof(self):
|
||||
self.amps = [] # eligible amplitudes since proof start
|
||||
self.blocks = [] # block means
|
||||
self.settled_at = None # block index where R held
|
||||
self.window = None # completed scoring cycles (list of dicts)
|
||||
|
||||
def discard_current(self):
|
||||
self._cur, self._since = None, 0
|
||||
|
||||
def rule(self, k):
|
||||
"""R(k): block-mean change ≤ 2 % and last-five spread ≤ 1 %."""
|
||||
if k < 1:
|
||||
return False
|
||||
w0, w1 = self.blocks[k - 1], self.blocks[k]
|
||||
last = self.amps[(k + 1) * BLOCK - LAST5:(k + 1) * BLOCK]
|
||||
a5 = sum(last) / LAST5
|
||||
return abs(w1 - w0) / w0 <= WINDOW_TOL and max(abs(a - a5) for a in last) / a5 <= LAST5_TOL
|
||||
|
||||
def update(self, y):
|
||||
"""Feed y after a committed step; returns 'scored' when a stable window completes."""
|
||||
out = None
|
||||
crossing = self._prev < 0.0 <= y and self._since >= 2
|
||||
if crossing:
|
||||
if self._cur is not None:
|
||||
out = self._close()
|
||||
self._cur, self._since = dict(ymax=y, ymin=y, ysum=0.0, n=0), 0
|
||||
if self._cur is not None:
|
||||
c = self._cur
|
||||
c["ymax"], c["ymin"] = max(c["ymax"], y), min(c["ymin"], y)
|
||||
c["ysum"] += y
|
||||
c["n"] += 1
|
||||
self._since += 1
|
||||
self._prev = y
|
||||
return out
|
||||
|
||||
def _close(self):
|
||||
c = self._cur
|
||||
a = 0.5 * (c["ymax"] - c["ymin"]) / self.D
|
||||
cyc = dict(ymax=c["ymax"] / self.D, ymin=c["ymin"] / self.D,
|
||||
offset=c["ysum"] / c["n"] / self.D, amp=a, steps=c["n"], eligible=a >= ELIGIBLE)
|
||||
self.log.append(cyc)
|
||||
if not cyc["eligible"]:
|
||||
return None
|
||||
self.counted += 1
|
||||
self.amps.append(a)
|
||||
if self.settled_at is not None:
|
||||
self.window.append(cyc)
|
||||
if len(self.amps) % BLOCK:
|
||||
return None
|
||||
self.blocks.append(sum(self.amps[-BLOCK:]) / BLOCK)
|
||||
k = len(self.blocks) - 1
|
||||
if self.settled_at is None:
|
||||
if self.rule(k):
|
||||
self.settled_at, self.window = k, []
|
||||
return None
|
||||
if not self.rule(k): # scoring block failed: restart from it
|
||||
amps, blocks = self.amps[-BLOCK:], self.blocks[-1:]
|
||||
self.reset_proof()
|
||||
self.amps, self.blocks = amps, blocks
|
||||
return None
|
||||
return "scored" if k == self.settled_at + 2 else None
|
||||
|
||||
|
||||
def score(window):
|
||||
"""Target metric and reported quantities over the 20 scoring cycles."""
|
||||
a_max = max(max(abs(c["ymax"]), abs(c["ymin"])) for c in window)
|
||||
return dict(A_max=a_max, mean_amp=sum(c["amp"] for c in window) / len(window),
|
||||
ymax=max(c["ymax"] for c in window), ymin=min(c["ymin"] for c in window),
|
||||
offset=sum(c["offset"] for c in window) / len(window),
|
||||
T_resp=sum(c["steps"] for c in window) / len(window),
|
||||
within_band=abs(a_max - TARGET_A) <= BAND * TARGET_A)
|
||||
+10
-21
@@ -204,33 +204,24 @@ def edge_to_node_frame(points):
|
||||
|
||||
|
||||
def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
jets=(), hits=None, *, _shifted_blocked=False):
|
||||
jets=(), hits=None):
|
||||
"""Per-link prescribed surface velocity (n, 2) FP32 and aperture mask.
|
||||
|
||||
See _surface_links, which also returns the unit-amplitude jet shapes.
|
||||
"""
|
||||
return _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry, jets, hits,
|
||||
_shifted_blocked=_shifted_blocked)[:2]
|
||||
|
||||
|
||||
def _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
jets=(), hits=None, *, _shifted_blocked=False):
|
||||
"""(velocity, aperture, jet shape, jet id); the last two are None without jets.
|
||||
|
||||
Host-only and storage-neutral: rows follow the given physical cut-link
|
||||
order. Returns all None when neither an object carries surface
|
||||
order. Returns (None, None) when neither an object carries surface
|
||||
actuation nor a jet is given. Jets need true FP64 ``hits`` (n, 2) and may
|
||||
not share a link with each other or with a legacy aperture.
|
||||
Shifted storage is privately enabled only by the all-blocked stepper.
|
||||
"""
|
||||
enabled = [obj for obj in objects if obj.surface_actuation is not None]
|
||||
if not enabled and not jets:
|
||||
return None, None, None, None
|
||||
from .preprocess.flag_overlay import validate_cut_link_ownership
|
||||
validate_cut_link_ownership(flags, fluid_idx, dir, cfg.nq)
|
||||
return None, None
|
||||
from ..lbm.descriptors import FLUID, SOLID
|
||||
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
||||
raise ValueError("Surface cut links must have active fluid receivers")
|
||||
if len(np.unique(fluid_idx.astype(np.uint64)*cfg.nq+dir)) != len(dir):
|
||||
raise ValueError("Surface boundary requires unique cut-link writers")
|
||||
surface_velocity = np.zeros((len(dir), 2), np.float32)
|
||||
surface_aperture = np.zeros(len(dir), bool)
|
||||
shape = jet_id = None
|
||||
for obj in enabled:
|
||||
obj.validate_surface_actuation(cfg)
|
||||
selected = body_id == obj.obj_id
|
||||
@@ -243,9 +234,7 @@ def _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
if hits is None or len(hits) != len(dir):
|
||||
raise ValueError("Jets require true hit points row-aligned with the cut links")
|
||||
if (cfg is None or not cfg.is_d2q9 or cfg.store_precision != "FP32"
|
||||
or cfg.streaming != "double_buffer"
|
||||
or (cfg.ddf_shifting and not (_shifted_blocked is True
|
||||
and cfg.collision == "SRT" and not cfg.les_enabled))):
|
||||
or cfg.streaming != "double_buffer" or cfg.ddf_shifting):
|
||||
raise ValueError("Jets require D2Q9/FP32/unshifted/double_buffer")
|
||||
shape, jet_id = jet_link_shapes(jets, hits)
|
||||
driven = jet_id >= 0
|
||||
@@ -263,4 +252,4 @@ def _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
raise ValueError("Jet overlaps a SurfaceActuation aperture")
|
||||
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
||||
surface_aperture |= driven
|
||||
return surface_velocity, surface_aperture, shape, jet_id
|
||||
return surface_velocity, surface_aperture
|
||||
|
||||
@@ -11,7 +11,7 @@ Responsibilities:
|
||||
|
||||
Design::
|
||||
Packed ``obs`` layout matches ``generate_config(..., n_objects=count)`` and
|
||||
``config_obs.h`` macros. Host sizing uses :func:`~CelerisLab.cuda.compiler.obs_layout`.
|
||||
``config_obs.h`` macros. Host sizing uses :func:`~CelerisLab.cuda.compiler_v2.obs_layout`.
|
||||
|
||||
Pending edits are lightweight: only the "intent" (add / remove) is stored.
|
||||
All derived quantities (flags, compact lists, GPU buffers) are built at
|
||||
@@ -27,8 +27,8 @@ import pycuda.driver as cuda
|
||||
|
||||
from .objects import SimObject
|
||||
from .registry import BodyRegistry
|
||||
from .preprocess.flag_overlay import merge_flag_masks, validate_cut_link_ownership
|
||||
from ..cuda.compiler import obs_layout
|
||||
from .preprocess.flag_overlay import merge_flag_masks
|
||||
from ..cuda.compiler_v2 import obs_layout
|
||||
from ..cuda.obs_layout import ObsLayout
|
||||
from ..lbm.descriptors import (
|
||||
FLUID, SOLID,
|
||||
@@ -369,9 +369,9 @@ class ObjectManager:
|
||||
|
||||
Args:
|
||||
objects: List of SimObject instances to process.
|
||||
domain_flags: Optional uint16 flags for receiver/donor classification.
|
||||
domain_flags: Optional uint16 flags for donor-cell classification.
|
||||
sensor_cell_counts_out: Int array to fill with per-object sensor
|
||||
cell counts after the full geometry has passed validation.
|
||||
cell counts. If provided, ``fill(0)`` is called first.
|
||||
hits_out: List that receives one FP64 (n, 2) array of true hit
|
||||
points row-aligned with the cut-link columns (for Jets).
|
||||
|
||||
@@ -394,8 +394,8 @@ class ObjectManager:
|
||||
active_fluid = ((domain_flags & FLUID) != 0) & ((domain_flags & SOLID) == 0)
|
||||
force_owners: set[int] = set()
|
||||
|
||||
sensor_counts = (np.zeros_like(sensor_cell_counts_out)
|
||||
if sensor_cell_counts_out is not None else None)
|
||||
if sensor_cell_counts_out is not None:
|
||||
sensor_cell_counts_out.fill(0)
|
||||
|
||||
ez = None
|
||||
if self.cfg and self.cfg.is_d3q19:
|
||||
@@ -405,9 +405,7 @@ class ObjectManager:
|
||||
|
||||
for obj in objects:
|
||||
obj.validate_surface_actuation(self.cfg)
|
||||
# Moving circles never enter the packed static links (docs/moving_bodies.md, section 3).
|
||||
if (not obj.is_sensor) and (not obj.is_force_region) and obj.motion is None \
|
||||
and hasattr(obj, 'get_curved_list'):
|
||||
if (not obj.is_sensor) and (not obj.is_force_region) and hasattr(obj, 'get_curved_list'):
|
||||
(
|
||||
fluid_idx, dirs, q_vals, body_ids,
|
||||
rx_vals, ry_vals, rz_vals, fallback_vals
|
||||
@@ -432,9 +430,9 @@ class ObjectManager:
|
||||
if len(cells) > 0:
|
||||
s_cells.append(cells)
|
||||
s_ids.append(ids)
|
||||
if (sensor_counts is not None
|
||||
and 0 <= obj.obj_id < sensor_counts.size):
|
||||
sensor_counts[obj.obj_id] = int(len(cells))
|
||||
if (sensor_cell_counts_out is not None
|
||||
and 0 <= obj.obj_id < sensor_cell_counts_out.size):
|
||||
sensor_cell_counts_out[obj.obj_id] = int(len(cells))
|
||||
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
||||
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
||||
keep = active_fluid[cells]
|
||||
@@ -463,9 +461,8 @@ class ObjectManager:
|
||||
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
||||
|
||||
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
|
||||
# Passive walls need the same ownership guarantees as driven walls.
|
||||
# Overlapping solids are not a supported union-boundary operation.
|
||||
validate_cut_link_ownership(domain_flags, r_idx, r_dir, self.nq)
|
||||
if hits_out is not None:
|
||||
hits_out.append(r_hit)
|
||||
if self.jets or any(obj.surface_actuation is not None for obj in objects):
|
||||
# Reject unresolved apertures/ownership before sync or restart mutates state.
|
||||
from ..lbm.curved_links import CurvedLinkSoA
|
||||
@@ -473,11 +470,6 @@ class ObjectManager:
|
||||
links.assign_host(r_idx, r_dir, r_q, r_bid, r_rx, r_ry, r_rz, r_fallback)
|
||||
links.assign_surface(objects, self.cfg, domain_flags, jets=self.jets, hits=r_hit)
|
||||
|
||||
if sensor_cell_counts_out is not None:
|
||||
sensor_cell_counts_out[:] = sensor_counts
|
||||
if hits_out is not None:
|
||||
hits_out.append(r_hit)
|
||||
|
||||
return (
|
||||
r_idx, r_dir, r_q, r_bid,
|
||||
r_rx, r_ry, r_rz, r_fallback,
|
||||
@@ -495,6 +487,8 @@ class ObjectManager:
|
||||
the caller has already prepared and uploaded the final flag field
|
||||
for this geometry, such as during initialization.
|
||||
"""
|
||||
self._telemetry_field = field
|
||||
|
||||
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
||||
hits = []
|
||||
|
||||
@@ -505,7 +499,6 @@ class ObjectManager:
|
||||
fr_cells, fr_obj_id,
|
||||
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
||||
|
||||
self._telemetry_field = field
|
||||
if rebuild_flags:
|
||||
field.flag = next_flags
|
||||
field.upload_flags()
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
# CelerisLab/body/motion.py
|
||||
"""Translation targets and device tables (dense double buffer, D2Q9 only).
|
||||
|
||||
Contract: docs/moving_bodies.md, sections 4a, 5 and 7. The applied geometry
|
||||
(last successfully committed offset) and an optional pending target are two
|
||||
separate facts; centres are always ``base + offset``. MovingCircle owns host
|
||||
targets, MovingTables owns device records, and Simulation commits completed steps.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional, Tuple
|
||||
|
||||
MAX_STEP = 0.5 # Euclidean centre displacement per step (lattice cells)
|
||||
BAND_MARGIN = 2 # cells added around the swept circle AABB
|
||||
SHARED_CLEARANCE = 4.0 # > two diagonal links + MAX_STEP; docs/steady_pinball_fiv.md
|
||||
R_MIN = 4.0 # smallest radius covered by the donor enumeration
|
||||
|
||||
Vec = Tuple[float, float]
|
||||
|
||||
|
||||
def _vec(value, name: str) -> Vec:
|
||||
x, y = (float(v) for v in value)
|
||||
if not (math.isfinite(x) and math.isfinite(y)):
|
||||
raise ValueError(f"{name} must be finite, got {value!r}")
|
||||
return x, y
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Translation:
|
||||
"""Declared motion envelope: offset box (x_min, x_max, y_min, y_max) and speed bound."""
|
||||
offset_box: Tuple[float, float, float, float]
|
||||
max_speed: float
|
||||
shared: bool = False # same envelope instance: synchronous transverse motion
|
||||
|
||||
def __post_init__(self):
|
||||
box = tuple(float(v) for v in self.offset_box)
|
||||
speed = float(self.max_speed)
|
||||
if len(box) != 4 or not all(math.isfinite(v) for v in box + (speed,)):
|
||||
raise ValueError("offset_box needs 4 finite values and max_speed must be finite")
|
||||
if not (box[0] <= 0.0 <= box[1] and box[2] <= 0.0 <= box[3]):
|
||||
raise ValueError("offset_box must contain the zero offset")
|
||||
if not 0.0 < speed <= MAX_STEP:
|
||||
raise ValueError(f"max_speed must satisfy 0 < max_speed <= {MAX_STEP}")
|
||||
if self.shared and box[:2] != (0.0, 0.0):
|
||||
raise ValueError("shared translation is transverse only (x offsets must be zero)")
|
||||
object.__setattr__(self, "offset_box", box)
|
||||
object.__setattr__(self, "max_speed", speed)
|
||||
|
||||
|
||||
def validate_shared_translations(circles):
|
||||
"""Check common old/target offsets and wall velocities before device work.
|
||||
|
||||
Per-body setters may stage members in any order. Envelope identity (not
|
||||
value equality) defines a group; a missing target means hold at applied.
|
||||
"""
|
||||
groups = {}
|
||||
for motion in circles:
|
||||
if not motion.translation.shared:
|
||||
continue
|
||||
target = motion.pending if motion.pending is not None else (motion.applied, (0.0, 0.0))
|
||||
state = (motion.applied, target)
|
||||
key = id(motion.translation)
|
||||
if groups.setdefault(key, state) != state:
|
||||
raise ValueError("shared translation needs identical applied/target offsets and velocities")
|
||||
|
||||
|
||||
class MovingCircle:
|
||||
"""Applied offset, pending target and the fail-closed commit of one circle."""
|
||||
|
||||
def __init__(self, base, radius: float, translation: Translation,
|
||||
on_failure: Optional[Callable[[BaseException], None]] = None):
|
||||
self.base = _vec(base, "base")
|
||||
self.radius = float(radius)
|
||||
if not (math.isfinite(self.radius) and self.radius >= R_MIN):
|
||||
raise ValueError(f"moving circle radius must be >= {R_MIN}")
|
||||
if not isinstance(translation, Translation):
|
||||
raise TypeError("translation must be a Translation")
|
||||
self.translation = translation
|
||||
self.applied: Vec = (0.0, 0.0)
|
||||
self.pending: Optional[Tuple[Vec, Vec]] = None
|
||||
self.failed = False
|
||||
self._on_failure = on_failure # owner lifecycle hook (Simulation marks itself failed)
|
||||
|
||||
def centre(self, offset: Vec) -> Vec:
|
||||
return self.base[0] + offset[0], self.base[1] + offset[1]
|
||||
|
||||
def check_target(self, offset, velocity) -> Tuple[Vec, Vec]:
|
||||
"""Validate a target against ``applied``; returns it, mutates nothing."""
|
||||
self._require_usable()
|
||||
offset, velocity = _vec(offset, "offset"), _vec(velocity, "velocity")
|
||||
x0, x1, y0, y1 = self.translation.offset_box
|
||||
if not (x0 <= offset[0] <= x1 and y0 <= offset[1] <= y1):
|
||||
raise ValueError(f"offset {offset} outside declared box {self.translation.offset_box}")
|
||||
if self.translation.shared and velocity[0] != 0.0:
|
||||
raise ValueError("shared translation needs zero x velocity")
|
||||
step = math.hypot(offset[0] - self.applied[0], offset[1] - self.applied[1])
|
||||
if step > MAX_STEP:
|
||||
raise ValueError(f"per-step displacement {step} exceeds {MAX_STEP}")
|
||||
if math.hypot(*velocity) > self.translation.max_speed:
|
||||
raise ValueError(f"velocity {velocity} exceeds max_speed {self.translation.max_speed}")
|
||||
if offset == self.applied and velocity != (0.0, 0.0):
|
||||
raise ValueError("a hold target (offset == applied) needs zero translational velocity")
|
||||
return offset, velocity
|
||||
|
||||
def set_target(self, offset, velocity) -> None:
|
||||
"""Replace the pending target (last wins); invalid input keeps the old one."""
|
||||
self.pending = self.check_target(offset, velocity)
|
||||
|
||||
def record(self) -> Tuple[Vec, Vec, Vec]:
|
||||
"""Motion record (c_old, c_new, u) for the next single moving step."""
|
||||
self._require_usable()
|
||||
if self.pending is not None:
|
||||
offset, velocity = self.pending
|
||||
else:
|
||||
offset, velocity = self.applied, (0.0, 0.0)
|
||||
return self.centre(self.applied), self.centre(offset), velocity
|
||||
|
||||
def commit(self) -> None:
|
||||
"""Applied := pending offset (caller has synchronized a successful step)."""
|
||||
if self.pending is not None:
|
||||
self.applied = self.pending[0]
|
||||
self.pending = None
|
||||
|
||||
def fail(self, exc: BaseException) -> None:
|
||||
self.failed = True
|
||||
if self._on_failure is not None:
|
||||
self._on_failure(exc)
|
||||
|
||||
def band(self) -> Tuple[int, int, int, int]:
|
||||
"""Inclusive cell AABB (x0, x1, y0, y1) swept over the whole offset box."""
|
||||
x0, x1, y0, y1 = self.translation.offset_box
|
||||
r, m = self.radius, BAND_MARGIN
|
||||
return (math.floor(self.base[0] + x0 - r) - m, math.ceil(self.base[0] + x1 + r) + m,
|
||||
math.floor(self.base[1] + y0 - r) - m, math.ceil(self.base[1] + y1 + r) + m)
|
||||
|
||||
def _require_usable(self) -> None:
|
||||
if self.failed:
|
||||
raise RuntimeError("moving circle state is unknown after a failed step")
|
||||
|
||||
|
||||
class MovingTables:
|
||||
"""Device tables of the translating circles for ``MovingCircleCells``/``Links``.
|
||||
|
||||
Band table (x0, y0, width, height, first_slot, body_id), immutable per run;
|
||||
FP64 motion records (cx_old, cy_old, cx_new, cy_new, r, ux, uy), immutable
|
||||
during a step; separate per-body diagnostics (see DIAG), never part of obs.
|
||||
"""
|
||||
BAND_INTS, GEOM_DOUBLES = 6, 7
|
||||
DIAG = ("cme_x", "cme_y", "wen_x", "wen_y", "link_dm", "n_links",
|
||||
"cover_jx", "cover_jy", "cover_mass", "uncover_jx", "uncover_jy", "uncover_mass",
|
||||
"eq_only", "no_donor", "crossings")
|
||||
DIAG_FLOATS = len(DIAG)
|
||||
|
||||
def __init__(self, band, geom):
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
self.n_bodies = len(geom) // self.GEOM_DOUBLES
|
||||
self.n_slots = int(band[-2] + band[-4] * band[-3] * 8)
|
||||
self.n_cells = self.n_slots // 8
|
||||
self.geom = list(geom) # host copy of the uploaded records
|
||||
self.band_gpu = cuda.to_device(np.asarray(band, np.int32))
|
||||
self.geom_gpu = cuda.to_device(np.asarray(geom, np.float64))
|
||||
# Owned page-locked staging for async record uploads (allocated once).
|
||||
self._geom_pinned = cuda.pagelocked_empty(len(geom), np.float64)
|
||||
self.diag_gpu = cuda.to_device(np.zeros(self.n_bodies * self.DIAG_FLOATS, np.float32))
|
||||
self.slot_debug_gpu = None # optional (q, fallback, rx, ry) per slot for parity tests
|
||||
|
||||
@classmethod
|
||||
def upload(cls, circles):
|
||||
"""``circles``: [(MovingCircle, body_id)] in body order."""
|
||||
return cls(*cls.layout(circles))
|
||||
|
||||
@staticmethod
|
||||
def layout(circles):
|
||||
"""Host band and geometry tables (no device work)."""
|
||||
band, geom, first = [], [], 0
|
||||
for motion, body_id in circles:
|
||||
x0, x1, y0, y1 = motion.band()
|
||||
width, height = x1 - x0 + 1, y1 - y0 + 1
|
||||
band += [x0, y0, width, height, first, int(body_id)]
|
||||
first += width * height * 8
|
||||
geom += MovingTables.record(motion)
|
||||
return band, geom
|
||||
|
||||
@staticmethod
|
||||
def record(motion):
|
||||
"""FP64 motion record of one circle for its next step (hold if nothing pending)."""
|
||||
c_old, c_new, u = motion.record()
|
||||
return [*c_old, *c_new, motion.radius, *u]
|
||||
|
||||
def upload_records(self, circles, stream) -> bool:
|
||||
"""Enqueue the records when they differ from the device copy; True if advancing.
|
||||
|
||||
The pinned staging buffer is reused: callers synchronize ``stream`` after every
|
||||
upload before the next one (``Simulation._run_moving``), so no copy is in flight
|
||||
when it is rewritten. ``geom`` is the enqueued state; after a failed copy the
|
||||
owner is failed and the cache is never trusted again.
|
||||
"""
|
||||
import pycuda.driver as cuda
|
||||
geom = [v for motion, _ in circles for v in self.record(motion)]
|
||||
if geom != self.geom:
|
||||
self._geom_pinned[:] = geom
|
||||
cuda.memcpy_htod_async(self.geom_gpu, self._geom_pinned, stream)
|
||||
self.geom = geom
|
||||
g = self.GEOM_DOUBLES
|
||||
return any(geom[i:i + 2] != geom[i + 2:i + 4] for i in range(0, len(geom), g))
|
||||
|
||||
def enable_slot_debug(self):
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
self.slot_debug_gpu = cuda.to_device(np.zeros(self.n_slots * 4, np.float32))
|
||||
|
||||
def zero_diagnostics(self, stream):
|
||||
import pycuda.driver as cuda
|
||||
cuda.memset_d32_async(self.diag_gpu, 0, self.n_bodies * self.DIAG_FLOATS, stream)
|
||||
|
||||
def download_diagnostics(self):
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
out = np.empty((self.n_bodies, self.DIAG_FLOATS), np.float32)
|
||||
cuda.memcpy_dtoh(out, self.diag_gpu)
|
||||
return out
|
||||
@@ -140,7 +140,6 @@ class SimObject:
|
||||
x=float(c[0]), y=float(c[1]), z=z0,
|
||||
)
|
||||
self.control = ObjectControl()
|
||||
self.motion = None # MovingCircle for a translating circle (body/motion.py)
|
||||
|
||||
@property
|
||||
def surface_actuation(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CelerisLab/body/preprocess/flag_overlay.py
|
||||
"""
|
||||
Flag overlay and cut-link ownership checks against the merged domain.
|
||||
Flag overlay: merge per-object flag masks onto a clean domain base.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,15 +12,6 @@ import numpy as np
|
||||
from ...lbm.descriptors import FLUID, SOLID, SENSOR_FLAG, FRC_REGION
|
||||
|
||||
|
||||
def validate_cut_link_ownership(flags, fluid_idx, directions, nq):
|
||||
"""Reject solid receivers and multiple writers to one population."""
|
||||
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
||||
raise ValueError("Cut links must have active fluid receivers")
|
||||
writers = fluid_idx.astype(np.uint64) * nq + directions
|
||||
if len(np.unique(writers)) != len(directions):
|
||||
raise ValueError("Boundary requires unique cut-link writers")
|
||||
|
||||
|
||||
def merge_flag_masks(
|
||||
base: np.ndarray,
|
||||
masks: List[np.ndarray],
|
||||
|
||||
@@ -7,7 +7,7 @@ Two JSON files:
|
||||
config_body.json – object list
|
||||
|
||||
LBMConfig / BodyConfig are plain dataclasses; they translate to
|
||||
macro dicts consumed by `cuda/compiler.py` for header generation.
|
||||
macro dicts consumed by `cuda/compiler_v2.py` for header generation.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
@@ -60,14 +60,14 @@ Python `config.py` 只负责读取和校验,不是配置位置。显式配置
|
||||
|
||||
**`lbm/kernels/config/*.h` 由编译器根据 `LBMConfig` 自动生成,请勿手改。**
|
||||
|
||||
仓库中检入的 `config/*.h` 可能是历史版本。`compiler.generate_config` 会复制 kernel 源码到独立临时构建目录,并在副本中生成配置;不会改写包内头文件或 PTX。返回目录须显式传给 `compile_kernel(build_dir=...)`,编译结果与配置摘要均保存在该目录。`Simulation` 负责目录清理;直接使用编译器的调用者须调用 `cleanup_build_dir()`。跳过生成配置直接编译仓库中的 `uniform.cu` 属不受支持用法。
|
||||
仓库中检入的 `config/*.h` 可能是历史版本。`compiler_v2.generate_config` 会复制 kernel 源码到独立临时构建目录,并在副本中生成配置;不会改写包内头文件或 PTX。返回目录须显式传给 `compile_kernel(build_dir=...)`,编译结果与配置摘要均保存在该目录。`Simulation` 负责目录清理;直接使用编译器的调用者须调用 `cleanup_build_dir()`。跳过生成配置直接编译仓库中的 `kernel_v2.cu` 属不受支持用法。
|
||||
|
||||
```
|
||||
config_lbm.json
|
||||
↓ load_lbm_config()
|
||||
config.py:LBMConfig (读取 + 校验)
|
||||
↓ to_macros()
|
||||
cuda/compiler.py (独立构建目录中生成 config/*.h)
|
||||
cuda/compiler_v2.py (独立构建目录中生成 config/*.h)
|
||||
↓
|
||||
config_grid.h → NX, NY, NZ, NT, LATTICE_MODEL;
|
||||
DIM / NQ 由 LATTICE_MODEL 推导写入(与 grid.lattice_model 一致,非 JSON 顶层字段)
|
||||
@@ -86,20 +86,6 @@ step/one_step_*.cu → kernel 编排
|
||||
- 转速 `omega` 属于运行时状态,可在步进期间更新;该更新不改变 `N_OBJS` 或 `OBS_*` 内存布局,因此**不需要**因 `omega` 变化触发重新编译。
|
||||
- 力矩观测从 `obs` 的 torque segment 读取;force / torque / sensor 段统一由 `config_obs.h` 宏描述。
|
||||
|
||||
### 外边界能力(Outer boundaries, dense `Simulation`)
|
||||
|
||||
The dense path supports one channel package, not four independently
|
||||
selectable sides:
|
||||
- left x = 0: velocity inlet (`method.inlet.scheme`, `profile`, `physics.velocity`);
|
||||
- right x = nx−1: outlet (`method.outlet.mode`; `neq_extrap` holds rho = 1 with
|
||||
a `max(u, 0)` backflow clamp when `backflow_clamp` is set);
|
||||
- bottom and top: one shared `y_wall_bc` (`bounce_back` halfway or
|
||||
`free_slip`); corners are walls.
|
||||
`y_wall_bc = "on_site_velocity"` switches to x-periodic with on-site y planes
|
||||
and requires an object-free domain. There is no x/y-periodic or symmetry
|
||||
option for domains with bodies; adding outer BC types is a separate design
|
||||
question, not covered by these keys.
|
||||
|
||||
### 稳定性默认窗口(Stability defaults)
|
||||
|
||||
- 默认 `method.omega_guard = {min: 0.01, max: 1.99}`,用于约束碰撞频率与 LES 有效粘度路径。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""CUDA build helpers (compilation itself does not need a live context)."""
|
||||
from . import compiler
|
||||
from . import compiler_v2 as compiler
|
||||
|
||||
__all__ = ["compiler"]
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# CelerisLab/cuda/compiler.py
|
||||
# CelerisLab/cuda/compiler_v2.py
|
||||
"""
|
||||
Kernel configuration, compilation, and module loading.
|
||||
|
||||
Owners call build_module(cfg, entry, arch=...), then set_params(module, omega, ...).
|
||||
build_module runs, in a private directory it removes afterwards:
|
||||
Workflow:
|
||||
1. build_dir = generate_config(lbm_cfg, n_objects) — isolated source snapshot
|
||||
2. compile_kernel(arch, source=entry, build_dir=build_dir) — nvcc -ptx
|
||||
2. compile_kernel(arch, build_dir=build_dir) — nvcc -ptx kernel_v2.cu
|
||||
3. load_module(ptx_path) — PyCUDA module from PTX
|
||||
Entries: uniform.cu (uniform grid), blocked.cu (every blocked level, single or refined).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -18,6 +16,7 @@ import shutil
|
||||
import tempfile
|
||||
import hashlib
|
||||
import json
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@@ -34,6 +33,9 @@ _KERNEL_DIR = os.path.join(
|
||||
)
|
||||
|
||||
|
||||
_last_build_dir = ContextVar("celeris_last_build_dir", default=None)
|
||||
_last_ptx_path = ContextVar("celeris_last_ptx_path", default=None)
|
||||
|
||||
|
||||
def kernel_path(filename: str) -> str:
|
||||
return os.path.join(_KERNEL_DIR, filename)
|
||||
@@ -52,16 +54,14 @@ def _write(path: str, content: str):
|
||||
|
||||
|
||||
def generate_config(cfg: LBMConfig, n_objects: int = 0,
|
||||
*, build_root: Optional[str] = None, block_root_bc: int = 0) -> str:
|
||||
*, build_root: Optional[str] = None) -> str:
|
||||
"""Snapshot kernels and render headers in a fresh private directory.
|
||||
|
||||
Returns its path; pass it to compile_kernel(build_dir=...). The caller owns cleanup_build_dir() once the loaded module is no longer
|
||||
Returns its path. Pass it explicitly to compile_kernel for interleaved
|
||||
builds. Legacy generate_config(); compile_kernel() remains context-local.
|
||||
The caller owns cleanup_build_dir() once the loaded module is no longer
|
||||
needed for recompilation/debugging. No package files are changed.
|
||||
block_root_bc specializes a blocked root module; fine/dense builds use 0.
|
||||
"""
|
||||
if (isinstance(block_root_bc, bool) or not isinstance(block_root_bc, int)
|
||||
or block_root_bc not in (0, 1, 2)):
|
||||
raise ValueError("block_root_bc must be an integer mode 0, 1 or 2")
|
||||
macros = cfg.to_macros()
|
||||
if isinstance(n_objects, bool) or not isinstance(n_objects, int) or n_objects < 0:
|
||||
raise ValueError("n_objects must be a nonnegative integer")
|
||||
@@ -71,19 +71,20 @@ def generate_config(cfg: LBMConfig, n_objects: int = 0,
|
||||
try:
|
||||
shutil.copytree(_KERNEL_DIR, build_dir, dirs_exist_ok=True,
|
||||
ignore=shutil.ignore_patterns("*.ptx", "*.cubin", "*.tmp"))
|
||||
_render_config(macros, n_objects, os.path.join(build_dir, "config"),
|
||||
block_root_bc=block_root_bc)
|
||||
_render_config(macros, n_objects, os.path.join(build_dir, "config"))
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(Path(build_dir).rglob("*")):
|
||||
if path.is_file():
|
||||
digest.update(str(path.relative_to(build_dir)).encode())
|
||||
digest.update(path.read_bytes())
|
||||
metadata = {"source_sha256": digest.hexdigest(), "config": asdict(cfg),
|
||||
"n_objects": n_objects, "block_root_bc": block_root_bc}
|
||||
"n_objects": n_objects}
|
||||
_write(os.path.join(build_dir, ".celeris-build"), json.dumps(metadata, sort_keys=True))
|
||||
except BaseException:
|
||||
shutil.rmtree(build_dir)
|
||||
raise
|
||||
_last_build_dir.set(build_dir)
|
||||
_last_ptx_path.set(None)
|
||||
return build_dir
|
||||
|
||||
|
||||
@@ -95,9 +96,12 @@ def cleanup_build_dir(build_dir: Optional[str]) -> None:
|
||||
if path == Path(_KERNEL_DIR).resolve() or not (path / ".celeris-build").is_file():
|
||||
raise ValueError("Refusing to remove a directory not owned by the compiler")
|
||||
shutil.rmtree(path)
|
||||
if _last_build_dir.get() == str(build_dir):
|
||||
_last_build_dir.set(None)
|
||||
_last_ptx_path.set(None)
|
||||
|
||||
|
||||
def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0):
|
||||
def _render_config(m: dict, n_objects: int, cdir: str):
|
||||
_write(os.path.join(cdir, "config_grid.h"), f"""{_HEADER}\
|
||||
// Layer: Global / Grid
|
||||
#ifndef CELERIS_CONFIG_GRID_H
|
||||
@@ -161,9 +165,6 @@ def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0
|
||||
#define STORE_PRECISION {m['STORE_PRECISION']}
|
||||
#define USE_DDF_SHIFTING {m['USE_DDF_SHIFTING']}
|
||||
|
||||
// Internal blocked root mode: 0=periodic/fine, 1=slot/reservoir, 2=channel.
|
||||
#define BLOCK_ROOT_BC {block_root_bc}
|
||||
|
||||
#define USE_LES {m['USE_LES']}
|
||||
#define LES_CS {m['LES_CS']}
|
||||
#define LES_CLOSED_FORM {m['LES_CLOSED_FORM']}
|
||||
@@ -240,9 +241,13 @@ def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compilation
|
||||
# ---------------------------------------------------------------------------
|
||||
def compile_kernel(arch: str = "sm_70", source: str = "uniform.cu",
|
||||
output: Optional[str] = None, *, build_dir: str) -> str:
|
||||
"""Compile the snapshot in build_dir (from generate_config); never package headers."""
|
||||
def compile_kernel(arch: str = "sm_70", source: str = "kernel_v2.cu",
|
||||
output: Optional[str] = None, *,
|
||||
build_dir: Optional[str] = None) -> str:
|
||||
"""Compile a generated private source snapshot; never use package headers."""
|
||||
# A failed attempt must not make implicit load_module() reuse older code.
|
||||
_last_ptx_path.set(None)
|
||||
build_dir = build_dir or _last_build_dir.get()
|
||||
if not build_dir or not (Path(build_dir) / ".celeris-build").is_file():
|
||||
raise RuntimeError("Call generate_config() and pass its build_dir before compiling")
|
||||
root = Path(build_dir).resolve()
|
||||
@@ -267,36 +272,23 @@ def compile_kernel(arch: str = "sm_70", source: str = "uniform.cu",
|
||||
finally:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
_last_ptx_path.set(str(dst))
|
||||
return str(dst)
|
||||
|
||||
|
||||
def load_module(ptx_path: str):
|
||||
"""Load one compiled PTX file as a new PyCUDA module."""
|
||||
def load_module(ptx_path: Optional[str] = None):
|
||||
"""Load explicit PTX or the last PTX compiled in this Python context."""
|
||||
ptx_path = ptx_path or _last_ptx_path.get()
|
||||
if not ptx_path:
|
||||
raise RuntimeError("No compiled PTX; call compile_kernel() first")
|
||||
import pycuda.driver as cuda
|
||||
return cuda.module_from_file(ptx_path)
|
||||
|
||||
|
||||
def build_module(cfg: LBMConfig, source: str, *, arch: str, n_objects: int = 0,
|
||||
block_root_bc: int = 0):
|
||||
"""Generate, compile and load one private module; the build directory is removed.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Back-compat helpers (used by test scripts)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
The single build path of every owner (Simulation, FixedPatchStepper levels).
|
||||
Each call returns a NEW module: callers must not share it, because its
|
||||
globals (d_params, ...) are mutable per module.
|
||||
"""
|
||||
build_dir = generate_config(cfg, n_objects=n_objects, block_root_bc=block_root_bc)
|
||||
try:
|
||||
return load_module(compile_kernel(arch, source=source, build_dir=build_dir))
|
||||
finally:
|
||||
cleanup_build_dir(build_dir)
|
||||
|
||||
|
||||
def set_params(module, omega: float, fx: float = 0.0, fy: float = 0.0, fz: float = 0.0,
|
||||
n_objects: int = 0) -> None:
|
||||
"""Write the module's __constant__ LBMParams (core/params.cuh layout)."""
|
||||
import struct
|
||||
import pycuda.driver as cuda
|
||||
ptr, size = module.get_global("d_params")
|
||||
if size != struct.calcsize("=ffffI"):
|
||||
raise RuntimeError("LBMParams host/device layout mismatch")
|
||||
cuda.memcpy_htod(ptr, struct.pack("=ffffI", omega, fx, fy, fz, n_objects))
|
||||
def compile_kernel_v2(arch: str = "sm_70") -> str:
|
||||
"""Alias for compile_kernel() kept for test-script compatibility."""
|
||||
return compile_kernel(arch=arch)
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Packed ``obs`` buffer layout for force / torque / sensor telemetry.
|
||||
|
||||
Usage::
|
||||
Host code (e.g. ``ObjectManager``) and ``compiler.generate_config`` must use
|
||||
Host code (e.g. ``ObjectManager``) and ``compiler_v2.generate_config`` must use
|
||||
the same ``dim`` and ``n_objects`` so allocation sizes match device macros.
|
||||
|
||||
Design::
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
# CelerisLab/lbm/block_stepper.py
|
||||
"""Opt-in periodic backend for one BlockLayout2D level, optional static circles.
|
||||
|
||||
D2Q9/SRT/FP32 unforced double buffers. Each step refreshes every halo from
|
||||
its owner core, then pulls/collides core cells into the spare buffer, then
|
||||
swaps. Only halo pair tables are uploaded; dense ownership stays on the host.
|
||||
The caller owns the CudaContext and must close this backend before it.
|
||||
Optional ``objects`` are stationary passive circles (static linear BFL via
|
||||
receiver-local staging: halo refresh -> cut links -> pull, no copy between).
|
||||
No force/actuation readout, motion, walls, refinement, checkpoint or runtime
|
||||
parameter mutation; wall/slot/inlet fields in cfg are ignored.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
from numbers import Integral
|
||||
import struct
|
||||
from threading import RLock
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
|
||||
from ..cuda import compiler_v2 as compiler
|
||||
from .blocks import NQ, BlockLayout2D, blocked_link_tables
|
||||
from .descriptors import D2Q9_EX, D2Q9_EY, FLUID, SOLID
|
||||
|
||||
MAX_GRID_DIM = 65535 # CUDA gridDim.y/z limit (rows, blocks)
|
||||
|
||||
|
||||
def _static_circles(layout, objects):
|
||||
"""Host tables for stationary circles; rejects before any CUDA work.
|
||||
|
||||
Every periodic neighbor of every fluid core is inspected, including domain
|
||||
edges that CircleGeometry does not enumerate, so clipped or incomplete
|
||||
geometry cannot pass. Each physical link has one receiver-owning block.
|
||||
"""
|
||||
from ..body.geometry.circle import CircleGeometry
|
||||
|
||||
ny, nx = layout.shape
|
||||
flags = np.full(ny * nx, FLUID, np.uint16)
|
||||
for obj in objects:
|
||||
if (type(obj.geometry) is not CircleGeometry or obj.is_sensor or obj.is_force_region
|
||||
or obj.surface_actuation is not None
|
||||
or any(getattr(obj.state, name) != 0 for name in ("vx", "vy", "omega"))
|
||||
or any(vars(obj.control).values())):
|
||||
raise ValueError("Block bodies must be stationary passive circles")
|
||||
cx, cy = obj.geometry.center
|
||||
radius = obj.geometry._radius
|
||||
if not np.isfinite([cx, cy, radius]).all() or radius <= 0:
|
||||
raise ValueError("Block circle center/radius must be finite with radius > 0")
|
||||
mask = obj.geometry.build_flag_mask(nx, ny)
|
||||
solid = (mask & SOLID) != 0
|
||||
if not solid.any() or np.any(flags[solid] & SOLID):
|
||||
raise ValueError("Block circles must be resolved and disjoint")
|
||||
flags[solid] = mask[solid]
|
||||
solid = ((flags & SOLID) != 0).reshape(ny, nx)
|
||||
if solid[[0, -1]].any() or solid[:, [0, -1]].any():
|
||||
raise ValueError("Block circles must not touch the domain edge")
|
||||
links = [link for obj in objects
|
||||
for link in obj.geometry.build_cut_links(nx, ny, domain_flags=flags)]
|
||||
def source(r, q):
|
||||
y, x = divmod(r, nx)
|
||||
return (y + D2Q9_EY[q]) % ny, (x + D2Q9_EX[q]) % nx
|
||||
receivers = {(int(link.fluid_idx), int(link.dir)) for link in links}
|
||||
required = {(int(r), q) for r in np.flatnonzero(~solid.ravel())
|
||||
for q in range(1, NQ) if solid[source(r, q)]}
|
||||
if len(receivers) != len(links) or receivers != required:
|
||||
raise ValueError("Cut links must cover every fluid pull from a solid exactly once")
|
||||
tables, _ = blocked_link_tables(
|
||||
layout, flags, np.array([link.fluid_idx for link in links], np.uint32),
|
||||
np.array([link.dir for link in links], np.uint8),
|
||||
np.array([link.q for link in links], np.float32),
|
||||
np.array([link.fallback for link in links], np.uint8))
|
||||
return dict(solid=solid, **tables)
|
||||
|
||||
|
||||
class PeriodicBlockStepper:
|
||||
"""G->G stepping of Q-major (9, ny, nx) postcollision populations.
|
||||
|
||||
On a device/launch failure the instance becomes unusable: close and
|
||||
recreate it rather than assuming a partially advanced buffer rolled back.
|
||||
"""
|
||||
def __init__(self, cfg, layout, *, context, objects=()):
|
||||
if not isinstance(layout, BlockLayout2D) or layout.periodic != (True, True):
|
||||
raise ValueError("PeriodicBlockStepper requires a doubly periodic BlockLayout2D")
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
or checked.streaming != "double_buffer" or checked.store_precision != "FP32"
|
||||
or checked.ddf_shifting or checked.les_enabled
|
||||
or (checked.ny, checked.nx) != layout.shape):
|
||||
raise ValueError("PeriodicBlockStepper requires matching D2Q9/SRT/FP32/"
|
||||
"unshifted/double_buffer config without LES")
|
||||
ly, lx = layout.local_shape
|
||||
pairs = len(layout.halo_dst)
|
||||
tpb = checked.threads_per_block
|
||||
if layout.nblocks > MAX_GRID_DIM or ly > MAX_GRID_DIM or not 0 < pairs < 2**31:
|
||||
raise ValueError("block count, block rows or halo pairs exceed launch limits")
|
||||
objects = tuple(objects) # an empty iterable takes the body-free path
|
||||
self.boundary = _static_circles(layout, objects) if objects else None
|
||||
links = 0 if self.boundary is None else len(self.boundary["cell"])
|
||||
if links >= 2**31:
|
||||
raise ValueError("cut links exceed launch limits")
|
||||
self.layout = layout
|
||||
self.ctx = context
|
||||
self._links = links
|
||||
self._link_grid = ((links + tpb - 1) // tpb, 1)
|
||||
# Wall/slot fields are not applied; neutralize them like the fine patch level.
|
||||
self._local_cfg = replace(checked, nx=lx, ny=ly, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||
self._block = (tpb, 1, 1)
|
||||
self._grid = ((lx + tpb - 1) // tpb, ly, layout.nblocks)
|
||||
self._copy_grid = ((pairs + tpb - 1) // tpb, 1)
|
||||
self._bytes = int(np.prod(layout.blocked_shape)) * 4
|
||||
self._lock = RLock()
|
||||
self._status = "new"
|
||||
self._step_count = 0
|
||||
self._allocations = []
|
||||
self._stream = self._module = self._copy = self._step = self._curved = None
|
||||
self._tables = {}
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
free, _ = cuda.mem_get_info()
|
||||
tables = sum(a.nbytes for k, a in (self.boundary or {}).items() if k != "solid")
|
||||
if 2 * self._bytes + layout.halo_dst.nbytes + layout.halo_src.nbytes + tables > free:
|
||||
raise MemoryError("blocked buffers exceed free device memory")
|
||||
self._stream = cuda.Stream()
|
||||
config = self._local_cfg
|
||||
config.validate()
|
||||
if not np.isclose(config.omega, 1 / (3 * checked.viscosity + .5), rtol=1e-14, atol=0):
|
||||
raise RuntimeError("block omega/tau mismatch")
|
||||
build = compiler.generate_config(config)
|
||||
try:
|
||||
ptx = compiler.compile_kernel(self.ctx.sm_arch, source="blocks.cu", build_dir=build)
|
||||
module = compiler.load_module(ptx)
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
ptr, size = module.get_global("d_params")
|
||||
if size != struct.calcsize("=ffffI"):
|
||||
raise RuntimeError("LBMParams host/device layout mismatch")
|
||||
cuda.memcpy_htod(ptr, struct.pack("=ffffI", config.omega, 0, 0, 0, 0))
|
||||
self._module = module
|
||||
self._current = self._allocate(self._bytes)
|
||||
self._spare = self._allocate(self._bytes)
|
||||
self._dst = self._upload(layout.halo_dst)
|
||||
self._src = self._upload(layout.halo_src)
|
||||
self._copy = module.get_function("BlockHaloCopy")
|
||||
self._step = module.get_function("BlockStep")
|
||||
if self.boundary is not None:
|
||||
self._tables = {k: self._upload(a) for k, a in self.boundary.items() if k != "solid"}
|
||||
self._curved = module.get_function("BlockCurvedBoundary")
|
||||
except BaseException:
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass # Preserve the original construction failure.
|
||||
raise
|
||||
|
||||
def _allocate(self, size):
|
||||
pointer = cuda.mem_alloc(size)
|
||||
self._allocations.append(pointer)
|
||||
return pointer
|
||||
|
||||
def _upload(self, array):
|
||||
array = np.ascontiguousarray(array)
|
||||
pointer = self._allocate(array.nbytes)
|
||||
cuda.memcpy_htod(pointer, array)
|
||||
return pointer
|
||||
|
||||
def _require(self, *states):
|
||||
if self._status not in states:
|
||||
raise RuntimeError(f"Block stepper is {self._status}; expected {' or '.join(states)}")
|
||||
|
||||
@property
|
||||
def step_count(self):
|
||||
return self._step_count
|
||||
|
||||
def initialize(self, g):
|
||||
"""Upload dense G_0; the spare buffer starts NaN so stale halos cannot hide."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("new", "ready")
|
||||
dense = np.array(g, dtype=np.float32, order="C", copy=True)
|
||||
if dense.shape != (NQ, *self.layout.shape):
|
||||
raise ValueError("Expected a Q-major (9, ny, nx) array")
|
||||
fluid = np.ones(self.layout.shape, bool) if self.boundary is None else ~self.boundary["solid"]
|
||||
if not np.isfinite(dense[:, fluid]).all() or (dense[:, fluid].sum(axis=0) <= 0).any():
|
||||
raise ValueError("Fluid populations must be finite with positive density")
|
||||
dense[:, ~fluid] = np.nan # Solid cores are deliberately invalid.
|
||||
blocked = self.layout.to_blocked(dense)
|
||||
spare = np.full_like(blocked, np.nan)
|
||||
try:
|
||||
cuda.memcpy_htod(self._current, blocked)
|
||||
cuda.memcpy_htod(self._spare, spare)
|
||||
self._step_count = 0
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def step(self, n=1):
|
||||
"""Advance n steps: halo refresh, then core pull/collide, per step."""
|
||||
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
|
||||
raise ValueError("n must be a nonnegative integer")
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
kw = dict(block=self._block, stream=self._stream)
|
||||
try:
|
||||
self._status = "advancing"
|
||||
for _ in range(n):
|
||||
self._copy(self._current, self._dst, self._src,
|
||||
np.uint32(len(self.layout.halo_dst)), grid=self._copy_grid, **kw)
|
||||
if self._curved is not None:
|
||||
t = self._tables
|
||||
self._curved(self._current, t["block"], t["cell"], t["dir"], t["q"],
|
||||
t["fallback"], np.uint32(self._links), np.uintp(0), # no actuation
|
||||
np.uintp(0), np.uintp(0), np.uintp(0), # no observation
|
||||
grid=self._link_grid, **kw)
|
||||
self._step(self._current, self._spare, np.uint32(self.layout.halo),
|
||||
self._tables.get("flags", np.uintp(0)), np.uintp(0), # no kind
|
||||
grid=self._grid, **kw)
|
||||
self._stream.synchronize()
|
||||
self._current, self._spare = self._spare, self._current
|
||||
self._step_count += 1
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def download(self):
|
||||
"""Return dense (9, ny, nx) FP32 core populations; no host cache."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
blocked = np.empty(self.layout.blocked_shape, np.float32)
|
||||
try:
|
||||
cuda.memcpy_dtoh(blocked, self._current)
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
return self.layout.to_dense(blocked)
|
||||
|
||||
def close(self):
|
||||
"""Release owned resources; retain them for retry if context activation fails."""
|
||||
with self._lock:
|
||||
if self._status == "closed":
|
||||
return
|
||||
self._status = "failed"
|
||||
with self.ctx.activate():
|
||||
self._status = "closed"
|
||||
error = None
|
||||
try:
|
||||
if self._stream is not None:
|
||||
self._stream.synchronize()
|
||||
except Exception as exc:
|
||||
error = exc
|
||||
self._copy = self._step = self._curved = self._module = None
|
||||
self._tables = {}
|
||||
for pointer in self._allocations:
|
||||
try:
|
||||
pointer.free()
|
||||
except Exception as exc:
|
||||
error = error or exc
|
||||
self._allocations.clear()
|
||||
self._stream = None
|
||||
if error is not None:
|
||||
raise error
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
if exc[0] is None:
|
||||
raise
|
||||
@@ -18,9 +18,7 @@ all NQ populations from an owner core cell into another block's halo cell.
|
||||
Periodicity is explicit per axis. Across a non-periodic exterior, halo cells
|
||||
have no pair and stay invalid (NaN after packing): no wrap, no implied BC.
|
||||
|
||||
Host preparation plus thin launch wrappers: no CUDA import or allocation
|
||||
(launch_* only call kernels and device tables the caller owns), no refinement
|
||||
levels; level_config and static_circle_tables prepare one blocked level.
|
||||
Preparation only: no CUDA, no boundary conditions, no refinement levels.
|
||||
The one-cell same-level halo here is unrelated to the four-cell fine halo of
|
||||
the fixed refinement patch (refinement.py), whose semantics are unchanged.
|
||||
|
||||
@@ -33,7 +31,7 @@ receiver-local boundary values: the solid's owner-core copy of that slot may
|
||||
be stale, which is distinct from the unique core ownership of stored state.
|
||||
No moving geometry is implied.
|
||||
"""
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import dataclass
|
||||
from numbers import Integral
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -375,84 +373,3 @@ def blocked_link_tables(layout, flags, fluid_idx, dir, q, fallback):
|
||||
dir=columns[1].astype(np.uint8), q=columns[2].astype(np.float32),
|
||||
fallback=columns[3].astype(np.uint8))
|
||||
return tables, mapped
|
||||
|
||||
|
||||
# One blocked level as FixedPatchStepper builds and launches it (any depth). Raw argument orders of the blocked.cu kernels live only here.
|
||||
|
||||
def level_config(cfg, layout, tau):
|
||||
"""Kernel config of one blocked level: NX/NY = block window incl. halo.
|
||||
|
||||
Physical wall/slot/inlet fields are neutralized; a blocked root gets them
|
||||
from its module globals (d_block_wall/d_block_channel) instead.
|
||||
"""
|
||||
ly, lx = layout.local_shape
|
||||
local = replace(cfg, nx=lx, ny=ly, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||
local.validate()
|
||||
if not np.isclose(local.omega, 1 / tau, rtol=1e-14, atol=0):
|
||||
raise RuntimeError("block level omega/tau mismatch")
|
||||
return local
|
||||
|
||||
|
||||
def launch_halo_copy(kernel, buffer, dst, src, count, *, block, stream):
|
||||
"""BlockHaloCopy over ``count`` uploaded (dst, src) block-cell pairs."""
|
||||
kernel(buffer, dst, src, np.uint32(count),
|
||||
grid=((count + block[0] - 1) // block[0], 1), block=block, stream=stream)
|
||||
|
||||
|
||||
def launch_curved_boundary(kernel, buffer, tables, count, *, block, stream,
|
||||
velocity=None, rx=None, ry=None, raw=None):
|
||||
"""BlockCurvedBoundary on uploaded link tables; None = no actuation/observation."""
|
||||
none = np.uintp(0)
|
||||
kernel(buffer, tables["block"], tables["cell"], tables["dir"], tables["q"], tables["fallback"],
|
||||
np.uint32(count), none if velocity is None else velocity,
|
||||
none if rx is None else rx, none if ry is None else ry, none if raw is None else raw,
|
||||
grid=((count + block[0] - 1) // block[0], 1), block=block, stream=stream)
|
||||
|
||||
|
||||
def static_circle_tables(layout, objects):
|
||||
"""Host tables for stationary circles; rejects before any CUDA work.
|
||||
|
||||
Every periodic neighbor of every fluid core is inspected, including domain
|
||||
edges that CircleGeometry does not enumerate, so clipped or incomplete
|
||||
geometry cannot pass. Each physical link has one receiver-owning block.
|
||||
"""
|
||||
from ..body.geometry.circle import CircleGeometry
|
||||
from .descriptors import FLUID, SOLID
|
||||
|
||||
ny, nx = layout.shape
|
||||
flags = np.full(ny * nx, FLUID, np.uint16)
|
||||
for obj in objects:
|
||||
if (type(obj.geometry) is not CircleGeometry or obj.is_sensor or obj.is_force_region
|
||||
or obj.surface_actuation is not None
|
||||
or any(getattr(obj.state, name) != 0 for name in ("vx", "vy", "omega"))
|
||||
or any(vars(obj.control).values())):
|
||||
raise ValueError("Block bodies must be stationary passive circles")
|
||||
cx, cy = obj.geometry.center
|
||||
radius = obj.geometry._radius
|
||||
if not np.isfinite([cx, cy, radius]).all() or radius <= 0:
|
||||
raise ValueError("Block circle center/radius must be finite with radius > 0")
|
||||
mask = obj.geometry.build_flag_mask(nx, ny)
|
||||
solid = (mask & SOLID) != 0
|
||||
if not solid.any() or np.any(flags[solid] & SOLID):
|
||||
raise ValueError("Block circles must be resolved and disjoint")
|
||||
flags[solid] = mask[solid]
|
||||
solid = ((flags & SOLID) != 0).reshape(ny, nx)
|
||||
if solid[[0, -1]].any() or solid[:, [0, -1]].any():
|
||||
raise ValueError("Block circles must not touch the domain edge")
|
||||
links = [link for obj in objects
|
||||
for link in obj.geometry.build_cut_links(nx, ny, domain_flags=flags)]
|
||||
def source(r, q):
|
||||
y, x = divmod(r, nx)
|
||||
return (y + D2Q9_EY[q]) % ny, (x + D2Q9_EX[q]) % nx
|
||||
receivers = {(int(link.fluid_idx), int(link.dir)) for link in links}
|
||||
required = {(int(r), q) for r in np.flatnonzero(~solid.ravel())
|
||||
for q in range(1, NQ) if solid[source(r, q)]}
|
||||
if len(receivers) != len(links) or receivers != required:
|
||||
raise ValueError("Cut links must cover every fluid pull from a solid exactly once")
|
||||
tables, _ = blocked_link_tables(
|
||||
layout, flags, np.array([link.fluid_idx for link in links], np.uint32),
|
||||
np.array([link.dir for link in links], np.uint8),
|
||||
np.array([link.q for link in links], np.float32),
|
||||
np.array([link.fallback for link in links], np.uint8))
|
||||
return dict(solid=solid, **tables)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Host contract for the blocked root's uniform SRT/MRT channel boundary.
|
||||
|
||||
Boundary ghosts are evolved state, but are not physical fluid leaves. Halo
|
||||
ownership remains periodic to reproduce the uniform kernel's wrapped pulls.
|
||||
No boundary closure is applied on a fine level.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
|
||||
def channel_masks(shape):
|
||||
"""Physical fluid and bulk-transfer masks in GLOBAL root coordinates."""
|
||||
ny, nx = shape
|
||||
fluid = np.zeros(shape, bool)
|
||||
fluid[1:ny - 1, 1:nx - 1] = True
|
||||
bulk = fluid.copy()
|
||||
bulk[[1, ny - 2]] = False
|
||||
return fluid, bulk
|
||||
|
||||
|
||||
def validate_channel_config(cfg, blocks):
|
||||
if any(block is None for block in blocks):
|
||||
raise ValueError("channel requires all-blocked levels")
|
||||
if (cfg.inlet_scheme != "zou_he_local" or cfg.outlet_mode != "neq_extrap"
|
||||
or cfg.y_wall_bc != "bounce_back" or cfg.top_wall_bc != "velocity"
|
||||
or cfg.wall_slot_width != 0 or cfg.wall_normal_velocity != 0):
|
||||
raise ValueError("channel requires Zou-He inlet, pressure NEQ outlet and stationary bounce-back y walls")
|
||||
|
||||
|
||||
def validate_channel_transfers(pair, layout, targets):
|
||||
"""Reject actual exchange/closure overlap, not a padded geometric envelope.
|
||||
|
||||
Explosion donors and restriction receivers must use ordinary bulk collision
|
||||
semantics. Restriction writes receiver-local source slots; these may not
|
||||
overwrite the channel's extra immutable-input reads (pressure donor and
|
||||
same-node halfway-wall corrections). The slot check deliberately respects
|
||||
block seams, rather than treating different halo images as the same slot.
|
||||
"""
|
||||
ny, nx = pair.coarse_shape
|
||||
_, bulk = channel_masks(pair.coarse_shape)
|
||||
legal = bulk.ravel() & ~pair.covered.ravel()
|
||||
receivers = pair.coalesce_coarse % (ny * nx)
|
||||
if not legal[pair.ghost_coarse].all() or not legal[receivers].all():
|
||||
raise ValueError("channel transfer donors and receivers must be uncovered bulk fluid")
|
||||
owner = layout.owner
|
||||
reads = []
|
||||
# Outlet closes from the full previous-step state at its left neighbor.
|
||||
donor = owner[1:-1, -1].astype(np.int64) - 1
|
||||
for q in range(9):
|
||||
reads.append(layout.slot(donor, q))
|
||||
for y, directions in ((1, (4, 6, 7)), (ny - 2, (3, 5, 8))):
|
||||
for q in directions:
|
||||
reads.append(layout.slot(owner[y, 1:-1], q))
|
||||
if np.intersect1d(targets, np.concatenate(reads)).size:
|
||||
raise ValueError("channel restriction writes overlap immutable boundary closure reads")
|
||||
@@ -94,10 +94,12 @@ class CurvedLinkSoA:
|
||||
"""
|
||||
self.surface_velocity = self.aperture = None
|
||||
self.jets, self.jet_shape, self.jet_id = tuple(jets), None, None
|
||||
from ..body.jets import _surface_links
|
||||
self.surface_velocity, self.aperture, self.jet_shape, self.jet_id = _surface_links(
|
||||
self.surface_velocity, self.aperture = surface_link_velocity(
|
||||
objects, cfg, flags, self.fluid_idx, self.dir, self.body_id, self.rx, self.ry,
|
||||
jets=self.jets, hits=hits)
|
||||
if self.jets:
|
||||
from ..body.jets import jet_link_shapes
|
||||
self.jet_shape, self.jet_id = jet_link_shapes(self.jets, hits)
|
||||
|
||||
def set_jet_amplitude(self, index, amplitude, stream=None):
|
||||
"""Rewrite jet rows from cached shapes; re-upload only the velocity column.
|
||||
|
||||
@@ -21,24 +21,7 @@ from .refinement import (ACTIVE, OUTER_GHOST, FixedPatch2D, FixedPatchSet2D,
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class Hierarchy:
|
||||
"""configs[l]: validated LBMConfig of level l; pairs[l]: tables of (l, l+1);
|
||||
kinds[l]: composed per-cell kind (child coverage OUTER on every non-finest level).
|
||||
|
||||
Index conventions (the single place they are defined):
|
||||
* level l = 0 (root) .. depth-1; pair index p = l-1 builds level l from level p;
|
||||
* array shapes are (ny, nx); rectangles, origins and centers are (x, y);
|
||||
* a pair's two members are its COARSE (parent) and FINE (child) level, for every
|
||||
pair, not absolute L0/L1 (fine_shape, fine_origin, blocked_coarse_tables, ...);
|
||||
* refined levels (l >= 1) of FixedPatchStepper place bodies and Jets in the root
|
||||
EDGE frame: root cell i covers [i, i+1), center i+.5. Uniform Simulation objects
|
||||
and the static circles of a single-level (depth 1) FixedPatchStepper use the
|
||||
integer-NODE frame (CircleGeometry.build_flag_mask at integer (x, y)). No
|
||||
NODE<->EDGE input conversion is applied anywhere; inside the EDGE frame, refined
|
||||
boundaries still project root-edge coordinates to fine-local cell indices with
|
||||
the half-cell shift ((v - o)/h - .5, see prepare_fine_boundary);
|
||||
* cell (i, j) of level l's allocated array (ghost band and holes included) covers
|
||||
[o + h*i, o + h*(i+1)) with (o, h) = edge_frame(l); level 1 is built from the root
|
||||
config directly, deeper levels from their parent frame (see edge_frame).
|
||||
"""
|
||||
kinds[l]: composed per-cell kind (child coverage OUTER on every non-finest level)."""
|
||||
configs: tuple
|
||||
pairs: tuple
|
||||
kinds: tuple
|
||||
@@ -47,18 +30,6 @@ class Hierarchy:
|
||||
def depth(self):
|
||||
return len(self.kinds)
|
||||
|
||||
def edge_frame(self, level):
|
||||
"""(origin (x, y), spacing) of level ``level``'s array in the root edge frame.
|
||||
|
||||
Each pair's fine_origin is in parent cells: zero for a set (level-wide grid),
|
||||
the shifted window origin for a scalar FixedPatch2D.
|
||||
"""
|
||||
origin, spacing = (0.0, 0.0), 1.0
|
||||
for pair in self.pairs[:level]:
|
||||
origin = tuple(o + spacing * f for o, f in zip(origin, pair.fine_origin))
|
||||
spacing /= 2
|
||||
return origin, spacing
|
||||
|
||||
|
||||
def is_rectangle(value):
|
||||
value = tuple(value)
|
||||
@@ -115,14 +86,14 @@ def _check_chain(coarse, fine, pair):
|
||||
def leaf_partition(h):
|
||||
"""Base-cell fraction published as ACTIVE over all levels (exact: powers of two).
|
||||
|
||||
Window (scalar) levels are offset by their fine_level_offset first.
|
||||
Window (scalar) levels are offset by their fine_level_origin first.
|
||||
"""
|
||||
base = np.zeros(h.kinds[0].shape)
|
||||
oy = ox = 0
|
||||
for level, kind in enumerate(h.kinds):
|
||||
if level:
|
||||
pair = h.pairs[level - 1]
|
||||
py, px = pair.fine_level_offset
|
||||
py, px = (pair.fine_level_origin if isinstance(pair, FixedPatch2D) else (0, 0))
|
||||
oy, ox = 2 * oy + py, 2 * ox + px
|
||||
y, x = np.nonzero(kind == ACTIVE)
|
||||
np.add.at(base, ((y + oy) >> level, (x + ox) >> level), 0.25 ** level)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// CelerisLab/lbm/kernels/blocked.cu
|
||||
// One fixed-size blocked level (lbm/blocks.py layout), specialized at compile
|
||||
// time for periodic/fine, slot/reservoir or channel stepping. Separate
|
||||
// compilation entry: NX, NY are the block window including halo, so index_f
|
||||
// and linear_index address one block; blockIdx.z selects the block, whose
|
||||
// populations start at b*NQ*TOTAL_CELLS (block-contiguous, Q-major inside).
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "core/params.cuh"
|
||||
#include "core/flags.cuh"
|
||||
#include "core/obs.cuh"
|
||||
#include "core/cell_kind.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#if COLLISION_MODEL == 2
|
||||
#include "operators/collision_mrt.cuh"
|
||||
#endif
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
|
||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require FP32 D2Q9 SRT or MRT double buffers without LES"
|
||||
#endif
|
||||
#ifndef BLOCK_ROOT_BC
|
||||
#error "Blocks require a generated BLOCK_ROOT_BC mode"
|
||||
#elif BLOCK_ROOT_BC < 0 || BLOCK_ROOT_BC > 2
|
||||
#error "BLOCK_ROOT_BC must be 0, 1 or 2"
|
||||
#endif
|
||||
|
||||
#if BLOCK_ROOT_BC == 1
|
||||
#include "boundary/block_wall.cuh"
|
||||
#endif
|
||||
#include "operators/block_collision.cuh"
|
||||
|
||||
// Kernels, in definition order: transfers, boundary staging, then the one full
|
||||
// step selected for this module (BlockStep, or BlockChannelStep for a channel root).
|
||||
#include "step/block_transfer.cu"
|
||||
#include "step/block_boundary.cu"
|
||||
#if BLOCK_ROOT_BC != 2
|
||||
#include "step/block_step.cu"
|
||||
#else
|
||||
#include "step/block_channel.cu"
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
// CelerisLab/lbm/kernels/blocks.cu
|
||||
// One periodic level of fixed-size blocks (lbm/blocks.py layout). Separate
|
||||
// compilation entry: NX, NY are the block window including halo, so index_f
|
||||
// and linear_index address one block; blockIdx.z selects the block, whose
|
||||
// populations start at b*NQ*TOTAL_CELLS (block-contiguous, Q-major inside).
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "core/params.cuh"
|
||||
#include "core/flags.cuh"
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
// Fixed-patch cell kinds, matching refinement.py ACTIVE/INNER_GHOST/OUTER_GHOST.
|
||||
constexpr unsigned char KIND_ACTIVE = 0u, KIND_INNER = 1u, KIND_OUTER = 2u;
|
||||
|
||||
// Zero-initialized for ordinary blocks/fine levels. Set only for parent walls;
|
||||
// NX/NY remain local storage strides, never physical wall coordinates.
|
||||
struct BlockWall { unsigned int nx, ny, slot_width; float amplitude; };
|
||||
extern "C" { __constant__ BlockWall d_block_wall; }
|
||||
|
||||
// Halo refresh: block cell ids b*TOTAL_CELLS+c, sources are always core cells.
|
||||
extern "C" __global__ void BlockHaloCopy(
|
||||
fpxx* f, const uint64_t* dst, const uint64_t* src, unsigned int count)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t d = dst[row], s = src[row];
|
||||
fpxx* to = f + (d / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
const fpxx* from = f + (s / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
for (unsigned int q = 0; q < NQ; ++q)
|
||||
to[index_f(d % TOTAL_CELLS, q)] = from[index_f(s % TOTAL_CELLS, q)];
|
||||
}
|
||||
|
||||
// Fixed-patch explosion into blocked fine storage: target = owner block cell
|
||||
// id of a patch-ghost cell; donors are parent block cell ids. Dense parent
|
||||
// storage is the one-window case with coarse_cells = full domain size.
|
||||
extern "C" __global__ void BlockExplode(
|
||||
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
|
||||
unsigned int count, unsigned long coarse_cells)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t t = target[row];
|
||||
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
const uint64_t d = donor[row];
|
||||
const fpxx* from = coarse + (d / coarse_cells) * NQ * coarse_cells;
|
||||
for (unsigned int q = 0; q < NQ; ++q)
|
||||
to[index_f(t % TOTAL_CELLS, q)] = from[q * coarse_cells + d % coarse_cells];
|
||||
}
|
||||
|
||||
// Targets are receiver-local SOURCE slots for the immediately following pull.
|
||||
// Keep child summation order identical to PatchCoarseStep; no collision here.
|
||||
extern "C" __global__ void BlockCoalesce(
|
||||
const fpxx* fine, fpxx* coarse, const uint64_t* target, const uint64_t* children,
|
||||
unsigned int count)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t* s = children + 4ul * row;
|
||||
coarse[target[row]] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
|
||||
}
|
||||
|
||||
// Static cut links, receiver-local staging: cell is the receiver's local core
|
||||
// cell, so the existing BFL writes the solid-source window image that this
|
||||
// block's next pull reads (no halo copy between this kernel and BlockStep).
|
||||
// surface_velocity (nullable, 2 floats per link): prescribed fluid velocity on
|
||||
// fixed geometry, same convention as CurvedBoundaryKernel. Optional raw rows
|
||||
// (jx,jy,torque,dm) are unique physical links, never body-observation slots.
|
||||
extern "C" __global__ void BlockCurvedBoundary(
|
||||
fpxx* f, const uint32_t* block, const uint32_t* cell, const unsigned char* dir,
|
||||
const float* q, const unsigned char* fallback, unsigned int count,
|
||||
const float* surface_velocity, const float* rx, const float* ry, float* raw)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
float Uw = 0.0f, Vw = 0.0f;
|
||||
if (surface_velocity != nullptr) {
|
||||
const float u = surface_velocity[2 * row], v = surface_velocity[2 * row + 1];
|
||||
if (u != 0.0f || v != 0.0f) { Uw = u; Vw = v; }
|
||||
}
|
||||
apply_bouzidi_link(dir[row], q[row], cell[row], f + (unsigned long)block[row] * NQ * TOTAL_CELLS,
|
||||
Uw, Vw, rx ? rx[row] : 0.0f, ry ? ry[row] : 0.0f,
|
||||
fallback[row], nullptr, -1, raw ? raw + 4ul * row : nullptr,
|
||||
raw ? raw + 4ul * row + 3 : nullptr);
|
||||
}
|
||||
|
||||
// Same-stream BFL -> this row-owned accumulation -> pull step. No atomics,
|
||||
// no cross-row reduction and no history allocation proportional to substeps.
|
||||
extern "C" __global__ void BlockAccumulateExchange(
|
||||
const float* raw, double* integral, unsigned int count, double h, double dt)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const unsigned long k = 4ul * row;
|
||||
const double impulse = h * dt;
|
||||
integral[k] += impulse * (double)raw[k];
|
||||
integral[k + 1] += impulse * (double)raw[k + 1];
|
||||
integral[k + 2] += h * impulse * (double)raw[k + 2];
|
||||
integral[k + 3] += h * h * (double)raw[k + 3];
|
||||
}
|
||||
|
||||
// Pull/collide/store core cells only; halo cells of out are left untouched.
|
||||
// flags (block cell ids, nullable): solid cores store NaN and never collide.
|
||||
// kind (block cell ids, nullable = all ACTIVE): OUTER skips the pull and
|
||||
// stores NaN, INNER pulls without collision, ACTIVE pulls and collides.
|
||||
extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int halo,
|
||||
const uint16_t* flags, const unsigned char* kind) {
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
|
||||
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
|
||||
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
|
||||
const unsigned char cell_kind = kind != nullptr ? kind[cell] : KIND_ACTIVE;
|
||||
float f[NQ];
|
||||
if (cell_kind == KIND_OUTER || (flags != nullptr && is_solid(flags[cell]))) {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
} else {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j); // Core neighbors stay inside the window: no wrap.
|
||||
stream_pull_load(k, f, in + base, j);
|
||||
if (cell_kind == KIND_ACTIVE) {
|
||||
if (d_block_wall.nx != 0) {
|
||||
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
|
||||
const unsigned int nbx = d_block_wall.nx / bx;
|
||||
const unsigned int gx = (blockIdx.z % nbx) * bx + x - halo;
|
||||
const unsigned int gy = (blockIdx.z / nbx) * by + y - halo;
|
||||
if (gy == 1)
|
||||
reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(
|
||||
gx, d_block_wall.nx, d_block_wall.slot_width, d_block_wall.amplitude));
|
||||
else if (gy == d_block_wall.ny - 2) reconstruct_top_reservoir(f, RHO);
|
||||
}
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
}
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Physical parent walls; NX/NY remain local storage strides.
|
||||
#ifndef CELERIS_BOUNDARY_BLOCK_WALL_CUH
|
||||
#define CELERIS_BOUNDARY_BLOCK_WALL_CUH
|
||||
|
||||
#include "planar_velocity.cuh"
|
||||
|
||||
struct BlockWall { unsigned int nx, ny, slot_width; float amplitude; };
|
||||
extern "C" { __constant__ BlockWall d_block_wall; }
|
||||
|
||||
__device__ __forceinline__ void block_wall(float* f, unsigned int x, unsigned int y,
|
||||
unsigned int halo) {
|
||||
unsigned int gx, gy;
|
||||
block_global_xy(x, y, halo, d_block_wall.nx, gx, gy);
|
||||
if (gy == 1)
|
||||
reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(
|
||||
gx, d_block_wall.nx, d_block_wall.slot_width, d_block_wall.amplitude));
|
||||
else if (gy == d_block_wall.ny - 2) reconstruct_top_reservoir(f, RHO);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -15,13 +15,10 @@
|
||||
// D2Q9 y-wall half-way bounce-back for pull double-buffer streaming.
|
||||
// ---------------------------------------------------------------------------
|
||||
#if LATTICE_MODEL == LATTICE_D2Q9
|
||||
// Rows 1 and top_row (= physical ny - 2) next to walls at rows 0 and ny - 1.
|
||||
// y is the physical row; k addresses the node in fi_in (a block window may
|
||||
// have NX/NY strides different from the physical extent).
|
||||
__device__ inline void apply_wall_bb_d2q9_rows(unsigned int y, unsigned int top_row,
|
||||
float* __restrict__ f,
|
||||
const fpxx* __restrict__ fi_in,
|
||||
unsigned long k)
|
||||
__device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
|
||||
float* __restrict__ f,
|
||||
const fpxx* __restrict__ fi_in,
|
||||
unsigned long k)
|
||||
{
|
||||
if (y == 1) {
|
||||
// Directions sourced from y=0 wall in pull step: +y, +x+y, -x+y.
|
||||
@@ -29,22 +26,14 @@ __device__ inline void apply_wall_bb_d2q9_rows(unsigned int y, unsigned int top_
|
||||
f[5] = load_ddf(fi_in, index_f(k, 6u));
|
||||
f[8] = load_ddf(fi_in, index_f(k, 7u));
|
||||
}
|
||||
else if (y == top_row) {
|
||||
// Directions sourced from the y = ny - 1 wall in pull step: -y, -x-y, +x-y.
|
||||
else if (y == (unsigned int)(NY - 2)) {
|
||||
// Directions sourced from y=NY-1 wall in pull step: -y, -x-y, +x-y.
|
||||
f[4] = load_ddf(fi_in, index_f(k, 3u));
|
||||
f[6] = load_ddf(fi_in, index_f(k, 5u));
|
||||
f[7] = load_ddf(fi_in, index_f(k, 8u));
|
||||
}
|
||||
}
|
||||
|
||||
__device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
|
||||
float* __restrict__ f,
|
||||
const fpxx* __restrict__ fi_in,
|
||||
unsigned long k)
|
||||
{
|
||||
apply_wall_bb_d2q9_rows(y, (unsigned int)(NY - 2), f, fi_in, k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// D2Q9 y-wall free-slip (specular reflection) for pull double-buffer streaming.
|
||||
//
|
||||
|
||||
@@ -27,22 +27,18 @@
|
||||
#define INLET_REGULARIZED_NEQ_DAMP 0.50f
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ float inlet_target_u(float y_coord, unsigned int ny, float velocity) {
|
||||
__device__ __forceinline__ float inlet_target_u(float y_coord) {
|
||||
#if INLET_PROFILE == 0
|
||||
return velocity;
|
||||
return U0;
|
||||
#else
|
||||
const float y_clamped = fminf((float)(ny - 2), fmaxf(1.0f, y_coord));
|
||||
const float H = fmaxf((float)(ny - 2), 1.0f);
|
||||
const float y_clamped = fminf((float)(NY - 2), fmaxf(1.0f, y_coord));
|
||||
const float H = fmaxf((float)(NY - 2), 1.0f);
|
||||
const float eta = (y_clamped - 0.5f) / H;
|
||||
const float shape = fmaxf(0.0f, 4.0f * eta * (1.0f - eta));
|
||||
return velocity * 1.5f * shape;
|
||||
return U0 * 1.5f * shape;
|
||||
#endif
|
||||
}
|
||||
|
||||
__device__ __forceinline__ float inlet_target_u(float y_coord) {
|
||||
return inlet_target_u(y_coord, NY, U0);
|
||||
}
|
||||
|
||||
#if LATTICE_MODEL == LATTICE_D2Q9
|
||||
__device__ __forceinline__ float west_velocity_rho_closure_d2q9(
|
||||
const float* __restrict__ f,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// On-site D2Q9 Zou-He velocity closure, paired lattice ordering.
|
||||
// Input is poststream f at the arrival time. Only unknown populations change;
|
||||
// the caller owns collision/store. No force or off-lattice walls. Shifted storage uses unit reference density.
|
||||
// the caller owns collision/store. No force, shifting or off-lattice walls.
|
||||
#ifndef CELERIS_BOUNDARY_PLANAR_VELOCITY_CUH
|
||||
#define CELERIS_BOUNDARY_PLANAR_VELOCITY_CUH
|
||||
#include <math.h>
|
||||
@@ -28,11 +28,7 @@ __device__ __forceinline__ float bottom_wall_normal_velocity(unsigned int x)
|
||||
|
||||
__device__ __forceinline__ void reconstruct_bottom_velocity(float* f, float u, float v)
|
||||
{
|
||||
const float rho = (
|
||||
#if USE_DDF_SHIFTING
|
||||
1.0f +
|
||||
#endif
|
||||
f[0] + f[1] + f[2] + 2.0f*(f[4] + f[6] + f[7])) / (1.0f-v);
|
||||
const float rho = (f[0] + f[1] + f[2] + 2.0f*(f[4] + f[6] + f[7])) / (1.0f-v);
|
||||
f[3] = f[4] + (2.0f/3.0f)*rho*v;
|
||||
f[5] = f[6] + 0.5f*(f[2]-f[1]) + 0.5f*rho*u + (1.0f/6.0f)*rho*v;
|
||||
f[8] = f[7] + 0.5f*(f[1]-f[2]) - 0.5f*rho*u + (1.0f/6.0f)*rho*v;
|
||||
@@ -40,11 +36,7 @@ __device__ __forceinline__ void reconstruct_bottom_velocity(float* f, float u, f
|
||||
|
||||
__device__ __forceinline__ void reconstruct_top_velocity(float* f, float u, float v)
|
||||
{
|
||||
const float rho = (
|
||||
#if USE_DDF_SHIFTING
|
||||
1.0f +
|
||||
#endif
|
||||
f[0] + f[1] + f[2] + 2.0f*(f[3] + f[5] + f[8])) / (1.0f+v);
|
||||
const float rho = (f[0] + f[1] + f[2] + 2.0f*(f[3] + f[5] + f[8])) / (1.0f+v);
|
||||
f[4] = f[3] - (2.0f/3.0f)*rho*v;
|
||||
f[6] = f[5] + 0.5f*(f[1]-f[2]) - 0.5f*rho*u - (1.0f/6.0f)*rho*v;
|
||||
f[7] = f[8] + 0.5f*(f[2]-f[1]) + 0.5f*rho*u - (1.0f/6.0f)*rho*v;
|
||||
@@ -54,12 +46,7 @@ __device__ __forceinline__ void reconstruct_top_velocity(float* f, float u, floa
|
||||
// J_y=rho*v follows from known poststream populations (ordinary D2Q9).
|
||||
__device__ __forceinline__ void reconstruct_top_reservoir(float* f, float rho)
|
||||
{
|
||||
#if USE_DDF_SHIFTING
|
||||
const float density_offset = rho - 1.0f;
|
||||
#else
|
||||
const float density_offset = rho;
|
||||
#endif
|
||||
const float jy = f[0]+f[1]+f[2]+2.0f*(f[3]+f[5]+f[8])-density_offset;
|
||||
const float jy = f[0]+f[1]+f[2]+2.0f*(f[3]+f[5]+f[8])-rho;
|
||||
f[4] = f[3] - (2.0f/3.0f)*jy;
|
||||
f[6] = f[5] + 0.5f*(f[1]-f[2]) - (1.0f/6.0f)*jy;
|
||||
f[7] = f[8] + 0.5f*(f[2]-f[1]) - (1.0f/6.0f)*jy;
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
#define STORE_PRECISION 0
|
||||
#define USE_DDF_SHIFTING 0
|
||||
|
||||
// Internal blocked root mode: 0=periodic/fine, 1=slot/reservoir, 2=channel.
|
||||
#define BLOCK_ROOT_BC 0
|
||||
|
||||
#define USE_LES 0
|
||||
#define LES_CS 0.160000f
|
||||
#define LES_CLOSED_FORM 1
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// CelerisLab – core/cell_kind.cuh
|
||||
// Fixed-patch cell kinds stored per block cell, matching lbm/refinement.py
|
||||
// ACTIVE/INNER_GHOST/OUTER_GHOST: OUTER is invalid (stores NaN, no pull),
|
||||
// INNER streams without collision, ACTIVE pulls and collides.
|
||||
#ifndef CELERIS_CORE_CELL_KIND_CUH
|
||||
#define CELERIS_CORE_CELL_KIND_CUH
|
||||
|
||||
constexpr unsigned char KIND_ACTIVE = 0u, KIND_INNER = 1u, KIND_OUTER = 2u;
|
||||
|
||||
// NaN populations mark a cell without valid state (OUTER ghost or solid core).
|
||||
__device__ __forceinline__ void fill_invalid(float* f)
|
||||
{
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -53,18 +53,6 @@ __device__ __forceinline__ void index_from_thread(unsigned int& x,
|
||||
k = (unsigned long)y * NX + x;
|
||||
}
|
||||
|
||||
// Global cell of window cell (x, y) in block blockIdx.z of a row-major,
|
||||
// full-domain block layout domain_nx cells wide. NX/NY are the local strides.
|
||||
__device__ __forceinline__ void block_global_xy(unsigned int x, unsigned int y,
|
||||
unsigned int halo, unsigned int domain_nx,
|
||||
unsigned int& gx, unsigned int& gy)
|
||||
{
|
||||
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
|
||||
const unsigned int nbx = domain_nx / bx;
|
||||
gx = (blockIdx.z % nbx) * bx + x - halo;
|
||||
gy = (blockIdx.z / nbx) * by + y - halo;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Neighbor list (periodic BCs via modular arithmetic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -44,4 +44,16 @@ struct RigidBodyControl2D {
|
||||
int control_mode; // 0=force, 1=velocity, 2=displacement
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Halo plan (multi-GPU / AMR 预留)
|
||||
// ============================================================================
|
||||
struct HaloPlan {
|
||||
int face_id;
|
||||
int peer_gpu;
|
||||
int level;
|
||||
size_t count;
|
||||
int* send_idx;
|
||||
int* recv_idx;
|
||||
};
|
||||
|
||||
#endif // CELERIS_CORE_PARAMS_CUH
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// CelerisLab – uniform.cu
|
||||
// CelerisLab – kernel_v2.cu
|
||||
// ============================================================================
|
||||
// Compilation entry point (Stage 2 Architecture).
|
||||
//
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
// File layout (each <300 lines):
|
||||
// operators/helpers.cuh — bounce_back_swap, collide_dispatch
|
||||
// step/init_flow.cu — InitTubeFlow, InitEsoPull
|
||||
// step/init_flow.cu — InitTubeFlow_v2, InitEsoPull
|
||||
// step/one_step_double.cu — OneStep (pull double-buffer)
|
||||
// step/one_step_esopull.cu — EsoPullStep (single-buffer)
|
||||
// step/aux_kernels.cu — CurvedBoundaryKernel, SensorKernel
|
||||
@@ -71,7 +71,6 @@ extern "C"
|
||||
#include "step/one_step_double.cu"
|
||||
#include "step/one_step_esopull.cu"
|
||||
#include "step/aux_kernels.cu"
|
||||
#include "step/moving_circle.cu"
|
||||
#include "step/esopull_macro.cu"
|
||||
#include "step/esopull_sync.cu"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// CelerisLab – operators/block_collision.cuh
|
||||
// The one collision extension point of the blocked steps (BlockStep, BlockChannelStep).
|
||||
// Unforced by construction: FixedPatchStepper supplies no forcing, so no runtime
|
||||
// d_params.fx/fy branch is read. Two compile-time arithmetic families:
|
||||
// uniform-equivalent (MRT on every level; SRT on the channel root, BLOCK_ROOT_BC 2):
|
||||
// collide_dispatch arithmetic with zero force and no LES: clamped omega;
|
||||
// SRT keeps the zero Guo forcing term of collide_srt;
|
||||
// level SRT (BLOCK_ROOT_BC 0/1): unclamped omega, no forcing term; shifted storage
|
||||
// builds feq from the retained drho and direct momentum (numerical variant).
|
||||
#ifndef CELERIS_OPERATORS_BLOCK_COLLISION_CUH
|
||||
#define CELERIS_OPERATORS_BLOCK_COLLISION_CUH
|
||||
|
||||
#include "../core/params.cuh"
|
||||
#include "macro.cuh"
|
||||
#include "collision_srt.cuh"
|
||||
#if COLLISION_MODEL == 2
|
||||
#include "collision_mrt.cuh"
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void block_collide(float* f) {
|
||||
float rho, ux, uy;
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
#if COLLISION_MODEL == 2 || BLOCK_ROOT_BC == 2
|
||||
const float omega = fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega));
|
||||
#if COLLISION_MODEL == 2
|
||||
collide_mrt_no_force(f, rho, ux, uy, omega);
|
||||
#else
|
||||
float feq[NQ], forcing[NQ] = {};
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt(f, feq, forcing, omega);
|
||||
#endif
|
||||
#else
|
||||
float feq[NQ];
|
||||
#if USE_DDF_SHIFTING
|
||||
// Preserve the density perturbation before adding the unit reference.
|
||||
const float drho = f[0]+f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8];
|
||||
const float jx = f[1]-f[2]+f[5]-f[6]+f[7]-f[8];
|
||||
const float jy = f[3]-f[4]+f[5]-f[6]-f[7]+f[8];
|
||||
compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);
|
||||
#else
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
#endif
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,14 +0,0 @@
|
||||
// CelerisLab – operators/bounce_back_swap.cuh
|
||||
// Full-way bounce-back at a wall node: swap every opposite pair (paired
|
||||
// order q, q+1). Needs only NQ; shared by the uniform and blocked steps.
|
||||
#ifndef CELERIS_OPERATORS_BOUNCE_BACK_SWAP_CUH
|
||||
#define CELERIS_OPERATORS_BOUNCE_BACK_SWAP_CUH
|
||||
|
||||
__device__ __forceinline__ void bounce_back_swap(float* f) {
|
||||
#pragma unroll
|
||||
for (int i = 1; i < NQ; i += 2) {
|
||||
float t = f[i]; f[i] = f[i+1]; f[i+1] = t;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -57,4 +57,25 @@ __device__ __forceinline__ void collide_trt(float* __restrict__ f,
|
||||
}
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void collide_trt_no_force(float* __restrict__ f,
|
||||
const float* __restrict__ feq,
|
||||
float omega)
|
||||
{
|
||||
const float wp = omega;
|
||||
const float wm = compute_omega_minus(wp);
|
||||
|
||||
f[0] = f[0] - wp * (f[0] - feq[0]);
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 1; i < NQ; i += 2) {
|
||||
const int ib = i + 1;
|
||||
const float fi = f[i], fb = f[ib];
|
||||
const float feqi = feq[i], feqb = feq[ib];
|
||||
const float delta_s = 0.5f * ((fi - feqi) + (fb - feqb));
|
||||
const float delta_a = 0.5f * ((fi - feqi) - (fb - feqb));
|
||||
f[i] = fi - wp * delta_s - wm * delta_a;
|
||||
f[ib] = fb - wp * delta_s + wm * delta_a;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CELERIS_OPERATORS_COLLISION_TRT_CUH
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
#ifndef CELERIS_OPERATORS_HELPERS_CUH
|
||||
#define CELERIS_OPERATORS_HELPERS_CUH
|
||||
|
||||
#include "bounce_back_swap.cuh"
|
||||
__device__ __forceinline__ void bounce_back_swap(float* f) {
|
||||
#pragma unroll
|
||||
for (int i = 1; i < NQ; i += 2) {
|
||||
float t = f[i]; f[i] = f[i+1]; f[i+1] = t;
|
||||
}
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void collide_dispatch(
|
||||
float* f, float rho_n,
|
||||
|
||||
@@ -64,12 +64,11 @@ __device__ __forceinline__ void compute_rho_u(const float* __restrict__ f,
|
||||
#if LATTICE_MODEL == LATTICE_D2Q9
|
||||
|
||||
__device__ __forceinline__ void compute_feq(float rho, float ux, float uy,
|
||||
float* __restrict__ feq
|
||||
#if USE_DDF_SHIFTING
|
||||
, float rhom1
|
||||
#endif
|
||||
)
|
||||
float* __restrict__ feq)
|
||||
{
|
||||
#if USE_DDF_SHIFTING
|
||||
const float rhom1 = rho - 1.0f; // perturbation density
|
||||
#endif
|
||||
const float ux3 = 3.0f * ux;
|
||||
const float uy3 = 3.0f * uy;
|
||||
const float c3 = -1.5f * (ux * ux + uy * uy); // = -|u|²/(2cs²)
|
||||
@@ -113,39 +112,6 @@ __device__ __forceinline__ void compute_feq(float rho, float ux, float uy,
|
||||
#undef FEQ_CORE
|
||||
}
|
||||
|
||||
#if USE_DDF_SHIFTING
|
||||
// Shifted D2Q9 equilibrium using input momentum in the linear term.
|
||||
// Algebraically identical to the usual second-order equilibrium; explicit
|
||||
// rational divisions avoid 3*w rounding and the rho*(j/rho) round trip.
|
||||
__device__ __forceinline__ void compute_feq_from_momentum(
|
||||
float delta, float jx, float jy, float ux, float uy,
|
||||
float* __restrict__ feq)
|
||||
{
|
||||
const float common = fmaf(-1.5f, fmaf(jx, ux, jy * uy), delta);
|
||||
feq[0] = W0_VAL * common;
|
||||
const float jp[4] = {jx, jy, jx + jy, jx - jy};
|
||||
const float up[4] = {ux, uy, ux + uy, ux - uy};
|
||||
#pragma unroll
|
||||
for (int p = 0; p < 4; ++p) {
|
||||
const float weight = p < 2 ? WS_VAL : WE_VAL;
|
||||
const float odd = __fdiv_rn(jp[p], p < 2 ? 3.0f : 12.0f);
|
||||
const float even = weight * fmaf(4.5f * jp[p], up[p], common);
|
||||
feq[1 + 2*p] = even + odd;
|
||||
feq[2 + 2*p] = even - odd;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_DDF_SHIFTING
|
||||
// Existing callers recover the offset from rho; blocked shifted collision
|
||||
// supplies sum(g) directly to the overload above, retaining small perturbations.
|
||||
__device__ __forceinline__ void compute_feq(float rho, float ux, float uy,
|
||||
float* __restrict__ feq)
|
||||
{
|
||||
compute_feq(rho, ux, uy, feq, rho - 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#elif LATTICE_MODEL == LATTICE_D3Q19
|
||||
|
||||
__device__ __forceinline__ void compute_feq(float rho, float ux, float uy, float uz,
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
// nu_t = (C_s * Delta)^2 * |S|, Delta = 1 (lattice unit)
|
||||
// omega_eff = 1 / (3*(nu + nu_t) + 0.5)
|
||||
//
|
||||
// |S| = sqrt(2 S_ij S_ij), with Delta = 1 in lattice units.
|
||||
// The stress estimate uses self-consistent tau_eff; tau0 is molecular.
|
||||
// Closed form uses full stress. Iteration uses deviatoric stress and a cap.
|
||||
// |S| is estimated from non-equilibrium second-order moments using tau0.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_OPERATORS_TURBULENCE_SMAG_CUH
|
||||
@@ -67,12 +65,12 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
|
||||
#if LES_CLOSED_FORM
|
||||
// Closed-form Smagorinsky (no iteration):
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·√2·Cs²·√Q / ρ))
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·Cs²·√Q / ρ))
|
||||
// where Q = Πneq_ij · Πneq_ij (Frobenius norm squared)
|
||||
const float Q = pixx*pixx + piyy*piyy + 2.0f*pixy*pixy;
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
@@ -142,7 +140,7 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
+ 2.0f*(pixy*pixy + pixz*pixz + piyz*piyz);
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Fixed, unforced internal 2:1 patch. Separate compilation entry;
|
||||
// default OneStep/channel kernels are deliberately not included.
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "core/params.cuh"
|
||||
#include "core/flags.cuh"
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
|
||||
extern "C" {
|
||||
#include "step/curved_double.cu"
|
||||
}
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Refinement requires unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
// Enabled only in the validation build; no diagnostic arrays in normal builds.
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
extern "C" {
|
||||
__device__ unsigned int collision_counts[TOTAL_CELLS];
|
||||
__device__ unsigned int explosion_counts[TOTAL_CELLS];
|
||||
__device__ unsigned int incoming_counts[NQ * TOTAL_CELLS];
|
||||
}
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void patch_collide(float* f, unsigned long k) {
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++collision_counts[k];
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" __global__ void PatchExplode(
|
||||
const float* coarse, float* fine, const uint32_t* targets,
|
||||
const uint32_t* donors, unsigned int count, unsigned long coarse_cells)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
for (unsigned int q = 0; q < NQ; ++q)
|
||||
fine[index_f(targets[row], q)] = coarse[q * coarse_cells + donors[row]];
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++explosion_counts[targets[row]];
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" __global__ void PatchFineStep(const float* in, float* out, const uint16_t* flags) {
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
float f[NQ];
|
||||
// Outer halo is invalid after EACH hop, including when reusing the buffer
|
||||
// that held explosion data. Inner halo only streams; it never collides.
|
||||
if (x < 2 || y < 2 || x >= NX - 2 || y >= NY - 2
|
||||
|| (flags != nullptr && is_solid(flags[k]))) {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
} else {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j); // These interior targets never wrap.
|
||||
stream_pull_load(k, f, in, j);
|
||||
if (x >= 4 && y >= 4 && x < NX - 4 && y < NY - 4)
|
||||
patch_collide(f, k);
|
||||
}
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void PatchCoarseStep(
|
||||
const float* in, float* out, const float* fine,
|
||||
const uint64_t* targets, const uint64_t* children, unsigned int count,
|
||||
unsigned int x0, unsigned int y0, unsigned int x1, unsigned int y1,
|
||||
unsigned int wall_aware)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
// Both buffers were poisoned at initialization; covered cells stay invalid.
|
||||
if (x >= x0 && x < x1 && y >= y0 && y < y1) return;
|
||||
if (wall_aware && (y == 0 || y == NY - 1)) return;
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j);
|
||||
float f[NQ];
|
||||
stream_pull_load(k, f, in, j);
|
||||
if (x >= x0 - 1 && x <= x1 && y >= y0 - 1 && y <= y1) {
|
||||
for (unsigned int q = 1; q < NQ; ++q) {
|
||||
uint64_t slot = index_f(k, q);
|
||||
unsigned int lo = 0, hi = count;
|
||||
while (lo < hi) {
|
||||
unsigned int mid = lo + (hi - lo) / 2;
|
||||
if (targets[mid] < slot) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo < count && targets[lo] == slot) {
|
||||
const uint64_t* s = children + 4ul * lo;
|
||||
f[q] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++incoming_counts[slot];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
// Includes the eight grazing-corner links with UNCOVERED coarse donors.
|
||||
// All replacement arrivals must be in place before computing rho/u.
|
||||
if (wall_aware) {
|
||||
if (y == 1) reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(x));
|
||||
else if (y == NY - 2) reconstruct_top_reservoir(f, RHO);
|
||||
}
|
||||
patch_collide(f, k);
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// ApplyEsoPullCurvedKernel, SensorKernel, ForceRegionKernel.
|
||||
//
|
||||
// Launched on compact lists (n_curved / n_sensor / n_cells threads).
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
//
|
||||
// Double-buffer mode:
|
||||
// CurvedBoundaryKernel runs BEFORE OneStep.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// CelerisLab/lbm/kernels/step/block_boundary.cu
|
||||
// Receiver-local curved-boundary staging and its raw-exchange accumulation.
|
||||
// Part of the blocked.cu compilation entry; relies on its includes and guards.
|
||||
|
||||
// Static cut links, receiver-local staging: cell is the receiver's local core
|
||||
// cell, so the existing BFL writes the solid-source window image that this
|
||||
// block's next pull reads (no halo copy between this kernel and BlockStep).
|
||||
// surface_velocity (nullable, 2 floats per link): prescribed fluid velocity on
|
||||
// fixed geometry, same convention as CurvedBoundaryKernel. Optional raw rows
|
||||
// (jx,jy,torque,dm) are unique physical links, never body-observation slots.
|
||||
extern "C" __global__ void BlockCurvedBoundary(
|
||||
fpxx* f, const uint32_t* block, const uint32_t* cell, const unsigned char* dir,
|
||||
const float* q, const unsigned char* fallback, unsigned int count,
|
||||
const float* surface_velocity, const float* rx, const float* ry, float* raw)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
float Uw = 0.0f, Vw = 0.0f;
|
||||
if (surface_velocity != nullptr) {
|
||||
const float u = surface_velocity[2 * row], v = surface_velocity[2 * row + 1];
|
||||
if (u != 0.0f || v != 0.0f) { Uw = u; Vw = v; }
|
||||
}
|
||||
apply_bouzidi_link(dir[row], q[row], cell[row], f + (unsigned long)block[row] * NQ * TOTAL_CELLS,
|
||||
Uw, Vw, rx ? rx[row] : 0.0f, ry ? ry[row] : 0.0f,
|
||||
fallback[row], nullptr, -1, raw ? raw + 4ul * row : nullptr,
|
||||
raw ? raw + 4ul * row + 3 : nullptr);
|
||||
}
|
||||
|
||||
// Same-stream BFL -> this row-owned accumulation -> pull step. No atomics,
|
||||
// no cross-row reduction and no history allocation proportional to substeps.
|
||||
extern "C" __global__ void BlockAccumulateExchange(
|
||||
const float* raw, double* integral, unsigned int count, double h, double dt)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const unsigned long k = 4ul * row;
|
||||
const double impulse = h * dt;
|
||||
integral[k] += impulse * (double)raw[k];
|
||||
integral[k + 1] += impulse * (double)raw[k + 1];
|
||||
integral[k + 2] += h * impulse * (double)raw[k + 2];
|
||||
integral[k + 3] += h * h * (double)raw[k + 3];
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#if BLOCK_ROOT_BC != 2
|
||||
#error "BlockChannelStep requires BLOCK_ROOT_BC == 2"
|
||||
#endif
|
||||
|
||||
// Root-only blocked counterpart of OneStep's D2Q9/SRT channel closure.
|
||||
// NX/NY are LOCAL strides; all physical boundary decisions use global coordinates.
|
||||
#include "../core/cell_kind.cuh"
|
||||
#include "../operators/bounce_back_swap.cuh"
|
||||
#include "../boundary/bounce_back.cuh"
|
||||
#include "../boundary/inlet/common.cuh"
|
||||
#include "../boundary/inlet/zou_he_local.cuh"
|
||||
#include "../boundary/outlet/pressure_neq.cuh"
|
||||
|
||||
struct BlockChannel { unsigned int nx, ny; float velocity; };
|
||||
extern "C" { __constant__ BlockChannel d_block_channel; }
|
||||
|
||||
// Same signature as BlockStep so the AMR hop/transfer schedule is unchanged.
|
||||
extern "C" __global__ void BlockChannelStep(
|
||||
const fpxx* in, fpxx* out, unsigned int halo,
|
||||
const uint16_t* flags, const unsigned char* kind)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
|
||||
unsigned int gx, gy;
|
||||
block_global_xy(x, y, halo, d_block_channel.nx, gx, gy);
|
||||
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
|
||||
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
|
||||
float f[NQ];
|
||||
if (kind != nullptr && kind[cell] == KIND_OUTER) {
|
||||
fill_invalid(f);
|
||||
} else {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j);
|
||||
stream_pull_load(k, f, in + base, j);
|
||||
const bool wall = gy == 0 || gy == d_block_channel.ny - 1;
|
||||
const bool outlet = gx == d_block_channel.nx - 1;
|
||||
if (wall) {
|
||||
bounce_back_swap(f); // Wall owns all four corners; ghosts never collide.
|
||||
} else if (gx == 0) {
|
||||
apply_zou_he_left_velocity_inlet_d2q9(
|
||||
f, inlet_target_u((float)gy, d_block_channel.ny, d_block_channel.velocity), 0.0f);
|
||||
} else if (outlet) {
|
||||
// Immutable input donor, receiver-local (also valid across a block seam).
|
||||
float donor[NQ];
|
||||
load_node(in + base, k - 1, donor);
|
||||
apply_pressure_outlet_d2q9(f, donor);
|
||||
} else {
|
||||
apply_wall_bb_d2q9_rows(gy, d_block_channel.ny - 2, f, in + base, k);
|
||||
}
|
||||
if (!wall && !outlet) block_collide(f);
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// CelerisLab/lbm/kernels/step/block_step.cu
|
||||
// Generic blocked pull/collide/store for periodic/fine and slot/reservoir levels.
|
||||
// Part of the blocked.cu compilation entry; relies on its includes and guards.
|
||||
#if BLOCK_ROOT_BC == 2
|
||||
#error "BlockStep is not built for the channel root (BLOCK_ROOT_BC == 2)"
|
||||
#endif
|
||||
|
||||
// Pull/collide/store core cells only; halo cells of out are left untouched.
|
||||
// flags (block cell ids, nullable): solid cores store NaN and never collide.
|
||||
// kind (block cell ids, nullable = all ACTIVE): OUTER skips the pull and
|
||||
// stores NaN, INNER pulls without collision, ACTIVE pulls and collides.
|
||||
extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int halo,
|
||||
const uint16_t* flags, const unsigned char* kind) {
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
|
||||
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
|
||||
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
|
||||
const unsigned char cell_kind = kind != nullptr ? kind[cell] : KIND_ACTIVE;
|
||||
float f[NQ];
|
||||
if (cell_kind == KIND_OUTER || (flags != nullptr && is_solid(flags[cell]))) {
|
||||
fill_invalid(f);
|
||||
} else {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j); // Core neighbors stay inside the window: no wrap.
|
||||
stream_pull_load(k, f, in + base, j);
|
||||
if (cell_kind == KIND_ACTIVE) {
|
||||
#if BLOCK_ROOT_BC == 1
|
||||
block_wall(f, x, y, halo);
|
||||
#endif
|
||||
block_collide(f);
|
||||
}
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// CelerisLab/lbm/kernels/step/block_transfer.cu
|
||||
// Same-level halo refresh and 2:1 explosion/coalescence between blocked levels.
|
||||
// Part of the blocked.cu compilation entry; relies on its includes and guards.
|
||||
|
||||
// Halo refresh: block cell ids b*TOTAL_CELLS+c, sources are always core cells.
|
||||
extern "C" __global__ void BlockHaloCopy(
|
||||
fpxx* f, const uint64_t* dst, const uint64_t* src, unsigned int count)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t d = dst[row], s = src[row];
|
||||
fpxx* to = f + (d / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
const fpxx* from = f + (s / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
for (unsigned int q = 0; q < NQ; ++q)
|
||||
to[index_f(d % TOTAL_CELLS, q)] = from[index_f(s % TOTAL_CELLS, q)];
|
||||
}
|
||||
|
||||
// Fixed-patch explosion into blocked fine storage: target = owner block cell
|
||||
// id of a patch-ghost cell; donors are parent block cell ids and coarse_cells
|
||||
// is the parent's block window size.
|
||||
extern "C" __global__ void BlockExplode(
|
||||
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
|
||||
unsigned int count, unsigned long coarse_cells)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t t = target[row];
|
||||
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
const uint64_t d = donor[row];
|
||||
const fpxx* from = coarse + (d / coarse_cells) * NQ * coarse_cells;
|
||||
for (unsigned int q = 0; q < NQ; ++q)
|
||||
to[index_f(t % TOTAL_CELLS, q)] = from[q * coarse_cells + d % coarse_cells];
|
||||
}
|
||||
|
||||
// Experimental Schornbaum--Ruede (arXiv:1508.07982, Algorithm 2)
|
||||
// and Chen et al. (doi:10.1016/j.physa.2005.09.036) population interpolation.
|
||||
// Independent implementation of the mathematical stencil; no upstream source copied.
|
||||
// Stencil cells are owner cores; a disabled axis repeats its center twice.
|
||||
extern "C" __global__ void BlockExplodeProjected(
|
||||
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
|
||||
unsigned int count, unsigned long coarse_cells,
|
||||
const uint64_t* neighbors, const float* offsets)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t t = target[row], d = donor[row];
|
||||
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
for (unsigned int q = 0; q < NQ; ++q) {
|
||||
float v[4];
|
||||
for (unsigned int a = 0; a < 4; ++a) {
|
||||
const uint64_t n = neighbors[4ul * row + a];
|
||||
v[a] = coarse[(n / coarse_cells) * NQ * coarse_cells
|
||||
+ q * coarse_cells + n % coarse_cells];
|
||||
}
|
||||
float gx = .5f * (v[1] - v[0]), gy = .5f * (v[3] - v[2]);
|
||||
const float ex = d_cx[q], ey = d_cy[q];
|
||||
if (q != 0) {
|
||||
const float along = (ex * gx + ey * gy) / (ex * ex + ey * ey);
|
||||
gx -= ex * along;
|
||||
gy -= ey * along;
|
||||
}
|
||||
to[index_f(t % TOTAL_CELLS, q)] =
|
||||
coarse[(d / coarse_cells) * NQ * coarse_cells + q * coarse_cells + d % coarse_cells]
|
||||
+ offsets[2ul * row] * gx + offsets[2ul * row + 1] * gy;
|
||||
}
|
||||
}
|
||||
|
||||
// Targets are receiver-local SOURCE slots for the immediately following pull.
|
||||
// Child summation order ((c0 + c1) + c2) + c3 is part of the numerics; no collision here.
|
||||
extern "C" __global__ void BlockCoalesce(
|
||||
const fpxx* fine, fpxx* coarse, const uint64_t* target, const uint64_t* children,
|
||||
unsigned int count)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t* s = children + 4ul * row;
|
||||
coarse[target[row]] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// Decodes the EsoPull backing layout into physical distributions, then
|
||||
// computes rho/ux/uy. Output is SoA: rho_out[N], ux_out[N], uy_out[N].
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
//
|
||||
// This kernel is the sole path for macroscopic diagnostics in EsoPull mode.
|
||||
// It uses the shared semantic helper layer (esopull_semantic_helpers.cuh).
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// or encodes physical DDF back to EsoPull backing layout.
|
||||
// Each thread handles one cell from a compact index list.
|
||||
//
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_STEP_ESOPULL_SYNC_CU
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// CelerisLab – step/init_flow.cu
|
||||
// Channel-flow initialization kernels (double-buffer and EsoPull).
|
||||
// Included by uniform.cu after all headers. No standalone compilation.
|
||||
// Included by kernel_v2.cu after all headers. No standalone compilation.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_STEP_INIT_FLOW_CU
|
||||
@@ -73,9 +73,9 @@ __device__ __forceinline__ uint16_t finalize_domain_flag(
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// InitTubeFlow — double-buffer channel initialization
|
||||
// InitTubeFlow_v2 — double-buffer channel initialization
|
||||
// ============================================================================
|
||||
__global__ void InitTubeFlow(uint16_t* flag, fpxx* fi)
|
||||
__global__ void InitTubeFlow_v2(uint16_t* flag, fpxx* fi)
|
||||
{
|
||||
#if DIM == 2
|
||||
unsigned int x, y; unsigned long k;
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
// Translating circles (docs/moving_bodies.md): M1 MovingCircleCells reclassifies
|
||||
// band cells and refills uncovered ones; M2 MovingCircleLinks applies the cut links.
|
||||
// Geometry comes from an immutable FP64 record per body
|
||||
// {cx_old, cy_old, cx_new, cy_new, r, ux, uy}; the M2 operation order mirrors
|
||||
// CircleGeometry.build_cut_links / find_circle_intersection with explicit _rn
|
||||
// operations (bit parity with the host builder is a test target, not a claim).
|
||||
// Moving circles are never in the packed static links, so this kernel is the
|
||||
// only writer of their solid slots.
|
||||
#ifndef CELERIS_MOVING_CIRCLE_CU
|
||||
#define CELERIS_MOVING_CIRCLE_CU
|
||||
|
||||
#if DIM == 2
|
||||
constexpr int MOVING_BAND_INTS = 6; // x0, y0, width, height, first_slot, body_id
|
||||
constexpr int MOVING_GEOM_DOUBLES = 7;
|
||||
// cme_x, cme_y, wen_x, wen_y, link_dm, n_links, cover_jx, cover_jy, cover_mass,
|
||||
// uncover_jx, uncover_jy, uncover_mass, eq_only, no_donor, crossings
|
||||
constexpr int MOVING_DIAG_FLOATS = 15;
|
||||
|
||||
__device__ __forceinline__ double mc_sq(double v) { return __dmul_rn(v, v); }
|
||||
|
||||
__device__ __forceinline__ bool mc_inside(int x, int y, double cx, double cy, double r)
|
||||
{
|
||||
return __dadd_rn(mc_sq(__dsub_rn((double)x, cx)), mc_sq(__dsub_rn((double)y, cy))) < mc_sq(r);
|
||||
}
|
||||
|
||||
// Smallest root t in [0, 1] of |p + t c - centre| = r (find_circle_intersection order).
|
||||
__device__ __forceinline__ bool mc_hit(int x, int y, int ex, int ey,
|
||||
double cx, double cy, double r, double& t)
|
||||
{
|
||||
const double dx = (double)ex, dy = (double)ey;
|
||||
const double px = __dsub_rn((double)x, cx), py = __dsub_rn((double)y, cy);
|
||||
const double a = __dadd_rn(mc_sq(dx), mc_sq(dy));
|
||||
const double b = __dmul_rn(2.0, __dadd_rn(__dmul_rn(dx, px), __dmul_rn(dy, py)));
|
||||
const double c = __dsub_rn(__dadd_rn(mc_sq(px), mc_sq(py)), mc_sq(r));
|
||||
const double det = __dsub_rn(mc_sq(b), __dmul_rn(__dmul_rn(4.0, a), c));
|
||||
if (det < 0.0) return false;
|
||||
const double s = __dsqrt_rn(det), a2 = __dmul_rn(2.0, a);
|
||||
const double t1 = __ddiv_rn(__dsub_rn(-b, s), a2);
|
||||
const double t2 = __ddiv_rn(__dadd_rn(-b, s), a2);
|
||||
if (0.0 <= t1 && t1 <= 1.0) { t = t1; return true; }
|
||||
if (0.0 <= t2 && t2 <= 1.0) { t = t2; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
__global__ void MovingCircleLinks(
|
||||
fpxx* fi, const uint16_t* flag,
|
||||
const int* band, const double* geom,
|
||||
unsigned int n_bodies, unsigned int n_slots,
|
||||
const float* action, float* obs, float* diag, float* slot_debug)
|
||||
{
|
||||
const unsigned int tid = threadIdx.x + blockIdx.x * blockDim.x;
|
||||
if (tid >= n_slots) return;
|
||||
unsigned int m = 0;
|
||||
while (m + 1 < n_bodies && tid >= (unsigned int)band[(m + 1) * MOVING_BAND_INTS + 4]) ++m;
|
||||
const int* b = band + m * MOVING_BAND_INTS;
|
||||
const unsigned int local = tid - (unsigned int)b[4];
|
||||
const unsigned int dir = local % 8u + 1u;
|
||||
const int x = b[0] + (int)((local / 8u) % (unsigned int)b[2]);
|
||||
const int y = b[1] + (int)((local / 8u) / (unsigned int)b[2]);
|
||||
const double* g = geom + MOVING_GEOM_DOUBLES * m;
|
||||
const double cx = g[2], cy = g[3], r = g[4];
|
||||
const int ex = d_cx[dir], ey = d_cy[dir];
|
||||
float* debug = slot_debug == nullptr ? nullptr : slot_debug + 4 * tid;
|
||||
if (debug != nullptr) debug[0] = -1.0f;
|
||||
|
||||
double t;
|
||||
if (mc_inside(x, y, cx, cy, r) || !mc_inside(x + ex, y + ey, cx, cy, r)
|
||||
|| !mc_hit(x, y, ex, ey, cx, cy, r, t))
|
||||
return;
|
||||
const double hx = __dadd_rn((double)x, __dmul_rn(t, (double)ex));
|
||||
const double hy = __dadd_rn((double)y, __dmul_rn(t, (double)ey));
|
||||
const double hdx = __dsub_rn(hx, (double)x), hdy = __dsub_rn(hy, (double)y);
|
||||
const double norm = __dsqrt_rn((double)(ex * ex + ey * ey));
|
||||
double qd = __ddiv_rn(__dsqrt_rn(__dadd_rn(mc_sq(hdx), mc_sq(hdy))), norm);
|
||||
qd = fmax(0.01, fmin(qd, 1.0));
|
||||
const float q = (float)qd;
|
||||
const float rx = (float)__dsub_rn(hx, cx), ry = (float)__dsub_rn(hy, cy);
|
||||
|
||||
// Same two-stage order as build_cut_links + pack_cut_links_to_soa: the fallback
|
||||
// class is chosen from the clamped FP64 q (q < 1/2 needs a fluid donor at x - c);
|
||||
// only then are q, rx, ry rounded to FP32 for the BFL branch and spin velocity.
|
||||
unsigned char fallback = CURVED_FALLBACK_BOUZIDI;
|
||||
if (qd < 0.5) {
|
||||
const int xd = x - ex, yd = y - ey;
|
||||
if (xd < 0 || xd >= (int)NX || yd < 0 || yd >= (int)NY || mc_inside(xd, yd, cx, cy, r)
|
||||
|| (flag[linear_index((unsigned int)xd, (unsigned int)yd)] & FLAG_FLUID) == 0)
|
||||
fallback = CURVED_FALLBACK_HALFWAY;
|
||||
}
|
||||
if (debug != nullptr) { debug[0] = q; debug[1] = (float)fallback; debug[2] = rx; debug[3] = ry; }
|
||||
|
||||
const int bid = b[5];
|
||||
const float omega = action_omega(action, bid);
|
||||
const float Uw = (float)g[5] - omega * ry, Vw = (float)g[6] + omega * rx; // u + omega x r
|
||||
float exchange[3], dm;
|
||||
apply_bouzidi_link(dir, q, linear_index((unsigned int)x, (unsigned int)y), fi,
|
||||
Uw, Vw, rx, ry, fallback, obs, bid, exchange, &dm);
|
||||
if (diag != nullptr) {
|
||||
float* d = diag + m * MOVING_DIAG_FLOATS;
|
||||
atomicAdd(&d[0], exchange[0]);
|
||||
atomicAdd(&d[1], exchange[1]);
|
||||
atomicAdd(&d[2], Uw * dm); // Wen 2014 correction -u_w (f_t - f_r)
|
||||
atomicAdd(&d[3], Vw * dm);
|
||||
atomicAdd(&d[4], dm);
|
||||
atomicAdd(&d[5], 1.0f);
|
||||
}
|
||||
}
|
||||
// M1: one thread per band cell, launched only on steps with c_old != c_new.
|
||||
// Membership comes from the immutable old/new predicates, never from flags, so
|
||||
// classification, refill and the flag write of one cell cannot race with another.
|
||||
// ddf holds post-collision G^{n-1}; refilled cells get all nine slots at that level.
|
||||
__global__ void MovingCircleCells(
|
||||
fpxx* fi, uint16_t* flag, const int* band, const double* geom,
|
||||
unsigned int n_bodies, unsigned int n_cells, const float* action, float* diag)
|
||||
{
|
||||
const unsigned int tid = threadIdx.x + blockIdx.x * blockDim.x;
|
||||
if (tid >= n_cells) return;
|
||||
unsigned int m = 0;
|
||||
while (m + 1 < n_bodies && tid >= (unsigned int)band[(m + 1) * MOVING_BAND_INTS + 4] / 8u) ++m;
|
||||
const int* b = band + m * MOVING_BAND_INTS;
|
||||
const unsigned int local = tid - (unsigned int)b[4] / 8u;
|
||||
const int x = b[0] + (int)(local % (unsigned int)b[2]);
|
||||
const int y = b[1] + (int)(local / (unsigned int)b[2]);
|
||||
const double* g = geom + MOVING_GEOM_DOUBLES * m;
|
||||
const bool in_old = mc_inside(x, y, g[0], g[1], g[4]);
|
||||
const bool in_new = mc_inside(x, y, g[2], g[3], g[4]);
|
||||
if (in_old == in_new) return;
|
||||
const unsigned long k = linear_index((unsigned int)x, (unsigned int)y);
|
||||
float* d = diag + m * MOVING_DIAG_FLOATS;
|
||||
float rho = 0.0f, jx = 0.0f, jy = 0.0f;
|
||||
if (in_new) { // covered: its fluid content leaves the active set
|
||||
for (unsigned int i = 0; i < NQ; ++i) {
|
||||
const float f = load_ddf(fi, index_f(k, i));
|
||||
rho += f; jx += (float)d_cx[i] * f; jy += (float)d_cy[i] * f;
|
||||
}
|
||||
flag[k] = FLAG_SOLID | FLAG_OBSTACLE | FLAG_BC_CURVED;
|
||||
atomicAdd(&d[6], jx); atomicAdd(&d[7], jy); atomicAdd(&d[8], rho);
|
||||
atomicAdd(&d[14], 1.0f);
|
||||
return;
|
||||
}
|
||||
// Uncovered: donors are fluid in BOTH geometries; NEQ from the neighbour with the
|
||||
// largest unnormalized c.n > 0 (first direction wins ties), n from c_new.
|
||||
const float nx = (float)__dsub_rn((double)x, g[2]), ny = (float)__dsub_rn((double)y, g[3]);
|
||||
float rho_sum = 0.0f, best_dot = 0.0f;
|
||||
int n_valid = 0, best = -1;
|
||||
for (unsigned int i = 1; i < NQ; ++i) {
|
||||
const int xn = x + d_cx[i], yn = y + d_cy[i];
|
||||
if (mc_inside(xn, yn, g[0], g[1], g[4]) || mc_inside(xn, yn, g[2], g[3], g[4])) continue;
|
||||
const unsigned long kn = linear_index((unsigned int)xn, (unsigned int)yn);
|
||||
for (unsigned int q = 0; q < NQ; ++q) rho_sum += load_ddf(fi, index_f(kn, q));
|
||||
++n_valid;
|
||||
const float dot = (float)d_cx[i] * nx + (float)d_cy[i] * ny;
|
||||
if (dot > best_dot) { best_dot = dot; best = (int)i; }
|
||||
}
|
||||
const float omega = action_omega(action, b[5]);
|
||||
const float uwx = (float)g[5] - omega * ny, uwy = (float)g[6] + omega * nx; // at the cell
|
||||
float feq[NQ], neq[NQ];
|
||||
compute_feq(n_valid > 0 ? rho_sum / (float)n_valid : (float)RHO, uwx, uwy, feq);
|
||||
for (unsigned int i = 0; i < NQ; ++i) neq[i] = 0.0f;
|
||||
if (best >= 0) {
|
||||
const unsigned long kd = linear_index((unsigned int)(x + d_cx[best]), (unsigned int)(y + d_cy[best]));
|
||||
float gd[NQ], rd = 0.0f, mx = 0.0f, my = 0.0f, fd[NQ];
|
||||
for (unsigned int i = 0; i < NQ; ++i) {
|
||||
gd[i] = load_ddf(fi, index_f(kd, i));
|
||||
rd += gd[i]; mx += (float)d_cx[i] * gd[i]; my += (float)d_cy[i] * gd[i];
|
||||
}
|
||||
compute_feq(rd, mx / rd, my / rd, fd);
|
||||
for (unsigned int i = 0; i < NQ; ++i) neq[i] = gd[i] - fd[i];
|
||||
} else {
|
||||
atomicAdd(&d[12], 1.0f);
|
||||
}
|
||||
if (n_valid == 0) atomicAdd(&d[13], 1.0f); // hard failure, checked before commit
|
||||
for (unsigned int i = 0; i < NQ; ++i) {
|
||||
const float f = feq[i] + neq[i];
|
||||
store_ddf(fi, index_f(k, i), f);
|
||||
rho += f; jx += (float)d_cx[i] * f; jy += (float)d_cy[i] * f;
|
||||
}
|
||||
flag[k] = FLAG_FLUID;
|
||||
atomicAdd(&d[9], jx); atomicAdd(&d[10], jy); atomicAdd(&d[11], rho);
|
||||
atomicAdd(&d[14], 1.0f);
|
||||
}
|
||||
#endif // DIM == 2
|
||||
|
||||
#endif // CELERIS_MOVING_CIRCLE_CU
|
||||
@@ -1,6 +1,6 @@
|
||||
// CelerisLab – step/one_step_double.cu
|
||||
// Double-buffer PULL streaming step kernel.
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_STEP_ONE_STEP_DOUBLE_CU
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// CelerisLab – step/one_step_esopull.cu
|
||||
// Esoteric-Pull single-buffer step kernel.
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
//
|
||||
// CURVED BC: supported via PrepareEsoPullCurvedKernel + ApplyEsoPullCurvedKernel
|
||||
// in aux_kernels.cu, launched by stepper.py before EsoPullStep.
|
||||
|
||||
@@ -21,14 +21,6 @@
|
||||
// i.e. the population traveling in direction i at node n
|
||||
// arrived from the node at n - c_i = neighbor in direction opp(i)
|
||||
// ---------------------------------------------------------------------------
|
||||
// load_node: all NQ populations of node n as stored (no streaming).
|
||||
__device__ __forceinline__ void load_node(const fpxx* __restrict__ fi, unsigned long n,
|
||||
float* __restrict__ f)
|
||||
{
|
||||
for (int q = 0; q < NQ; ++q)
|
||||
f[q] = load_ddf(fi, index_f(n, q));
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void stream_pull_load(unsigned long n,
|
||||
float* __restrict__ f,
|
||||
const fpxx* __restrict__ fi,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Private D2Q9 FP32 storage boundary for the opt-in blocked steppers."""
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
# Unit-density reference weights in FP64; sum(w)=1 matches rho=1+sum(g).
|
||||
_WEIGHTS = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36],
|
||||
dtype=np.float64)[:, None, None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class _StoredPopulations:
|
||||
values: tuple
|
||||
shifted: bool
|
||||
|
||||
|
||||
def _encode_physical(values, shifted):
|
||||
if not shifted:
|
||||
return np.array(values, dtype=np.float32, order="C", copy=True)
|
||||
return np.ascontiguousarray(np.asarray(values, dtype=np.float64) - _WEIGHTS,
|
||||
dtype=np.float32)
|
||||
|
||||
|
||||
def _decode_stored(values, shifted):
|
||||
return values.astype(np.float64) + _WEIGHTS if shifted else values
|
||||
@@ -7,26 +7,16 @@ import numpy as np
|
||||
|
||||
|
||||
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
time_end, window, reference_center, duration=None):
|
||||
time_end, window, reference_center):
|
||||
"""Reduce FP64 interval integrals or convert FP32 last-substep raw rows.
|
||||
|
||||
dt_l=h_l under acoustic 2:1 refinement. Local momentum j -> base force
|
||||
h*j -> impulse h*dt*j; torque adds one h; mass is h**2*dm.
|
||||
window="window" reduces an FP64 observation-window ledger spanning
|
||||
``duration`` (integer >= 1) base intervals; rates divide by that duration.
|
||||
"""
|
||||
if window not in ("last_base_step", "last_substep", "window"):
|
||||
if window not in ("last_base_step", "last_substep"):
|
||||
raise ValueError("unknown exchange window")
|
||||
if window == "window":
|
||||
if isinstance(duration, bool) or not isinstance(duration, (int, np.integer)) or duration < 1:
|
||||
raise ValueError("window duration must be an integer number of base intervals >= 1")
|
||||
elif duration is not None:
|
||||
raise ValueError("duration applies only to window='window'")
|
||||
values = np.array(rows, dtype=np.float64, copy=True)
|
||||
if window == "window":
|
||||
duration = float(duration)
|
||||
else:
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
if window == "last_substep":
|
||||
values *= np.array([h*h, h*h, h*h*h, h*h])
|
||||
selected = np.asarray(body_ids) == body_id
|
||||
@@ -41,7 +31,6 @@ def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
return dict(wall=reduce(selected & ~aperture), aperture=reduce(selected & aperture),
|
||||
total=reduce(selected), body_id=int(body_id), level=int(level),
|
||||
time_start=time_end-duration, time_end=time_end, dt=h,
|
||||
sample_count=(round(duration/h) if window == "window"
|
||||
else round(1/h) if window == "last_base_step" else 1),
|
||||
sample_count=round(1/h) if window == "last_base_step" else 1,
|
||||
window=window, units="base_lattice_per_unit_depth",
|
||||
definition="raw_boundary_exchange", reference_center=tuple(reference_center), valid=True)
|
||||
|
||||
@@ -52,17 +52,17 @@ class FixedPatch2D:
|
||||
def from_config(cls, cfg: LBMConfig, bounds) -> "FixedPatch2D":
|
||||
"""Prepare maps without changing cfg or allocating coarse-sized tables.
|
||||
|
||||
The backend contract is D2Q9/SRT or MRT/FP32/double_buffer,
|
||||
The initial backend contract is D2Q9/SRT/FP32/unshifted/double_buffer,
|
||||
without LES. Two coarse cells of margin keep all halo donors in bounds;
|
||||
this does NOT certify that those donors are fluid or periodic.
|
||||
"""
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision not in ("SRT", "MRT")
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32"
|
||||
or checked.les_enabled):
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT or MRT/FP32/double_buffer without LES")
|
||||
or checked.ddf_shifting or checked.les_enabled):
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT/FP32/unshifted/double_buffer without LES")
|
||||
bounds = tuple(bounds)
|
||||
if len(bounds) != 4 or any(isinstance(v, (bool, np.bool_))
|
||||
or not isinstance(v, Integral) for v in bounds):
|
||||
@@ -115,16 +115,10 @@ class FixedPatch2D:
|
||||
return self.bounds[0] - HALO / 2, self.bounds[1] - HALO / 2
|
||||
|
||||
@property
|
||||
def fine_level_offset(self) -> tuple[int, int]:
|
||||
"""Array index (y, x) of this window's first cell on the level-wide 2:1 fine grid
|
||||
(an index offset, not an (x, y) origin)."""
|
||||
def fine_level_origin(self) -> tuple[int, int]:
|
||||
"""(y, x) of this window's first cell on the level-wide 2:1 fine grid."""
|
||||
return 2 * self.bounds[1] - HALO, 2 * self.bounds[0] - HALO
|
||||
|
||||
@property
|
||||
def rectangles(self) -> tuple:
|
||||
"""The one parent rectangle, as a sequence like FixedPatchSet2D.rectangles."""
|
||||
return (self.bounds,)
|
||||
|
||||
@property
|
||||
def covered(self) -> np.ndarray:
|
||||
"""Dense coarse mask of cells replaced by the patch (not leaves)."""
|
||||
@@ -157,16 +151,20 @@ class FixedPatch2D:
|
||||
raise ValueError("levels are not synchronized: fine_steps must equal 2*coarse_steps")
|
||||
|
||||
|
||||
MIN_PATCH_GAP = 4 # coarse cells: the full 4-fine-cell supports stay disjoint
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class FixedPatchSet2D:
|
||||
"""Rectangles describing one 2:1 fine union, with unique transfer ownership.
|
||||
"""Several separated FixedPatch2D rectangles sharing one 2:1 fine level.
|
||||
|
||||
Input patches remain canonical metadata. Numerical kinds and tables are
|
||||
built once from their union; adjacent, overlapping and concave unions are
|
||||
accepted when their actual transfer dependencies close. Sparse storage owns
|
||||
every whole tile intersecting the support, independent of rectangle edges.
|
||||
The fine level is the whole (2*ny, 2*nx) grid in level coordinates; only
|
||||
the patch supports (active region + HALO ghost band) are stored, by a
|
||||
sparse BlockLayout2D from fine_layout(). Tables have FixedPatch2D meaning
|
||||
and row/child order, re-addressed to level cells: ghost_fine are level
|
||||
fine cells, coalesce_fine are q*N+cell level slots; coarse ids unchanged.
|
||||
Patches are kept in canonical (y0, x0) order, so input order is irrelevant.
|
||||
Active regions must be >= MIN_PATCH_GAP coarse cells apart (Chebyshev).
|
||||
"""
|
||||
coarse_shape: tuple[int, int]
|
||||
patches: tuple
|
||||
@@ -184,43 +182,42 @@ class FixedPatchSet2D:
|
||||
if not rectangles:
|
||||
raise ValueError("at least one patch rectangle is required")
|
||||
patches = sorted((FixedPatch2D.from_config(cfg, r) for r in rectangles),
|
||||
key=lambda p: (p.bounds[1], p.bounds[0], p.bounds[3], p.bounds[2]))
|
||||
key=lambda p: (p.bounds[1], p.bounds[0]))
|
||||
for i, a in enumerate(patches):
|
||||
for b in patches[i + 1:]:
|
||||
(ax0, ay0, ax1, ay1), (bx0, by0, bx1, by1) = a.bounds, b.bounds
|
||||
if max(ay0 - by1, by0 - ay1, ax0 - bx1, bx0 - ax1) < MIN_PATCH_GAP:
|
||||
raise ValueError(f"patches {a.bounds} and {b.bounds} are closer "
|
||||
f"than {MIN_PATCH_GAP} coarse cells")
|
||||
ny, nx = patches[0].coarse_shape
|
||||
covered = np.zeros((ny, nx), bool)
|
||||
for p in patches:
|
||||
covered |= p.covered
|
||||
fy, fx = 2 * ny, 2 * nx
|
||||
if fy * fx * 9 > np.iinfo(np.uint64).max // 2:
|
||||
raise ValueError("fine level exceeds 64-bit indexing")
|
||||
active = np.kron(covered, np.ones((2, 2), bool))
|
||||
inner = _dilate(active, 2)
|
||||
support = _dilate(active, HALO)
|
||||
kind = np.full((fy, fx), OUTER_GHOST, np.uint8)
|
||||
kind[inner] = INNER_GHOST
|
||||
kind[active] = ACTIVE
|
||||
gy, gx = np.nonzero(support & ~active)
|
||||
ghost_fine = (gy * fx + gx).astype(np.uint64)
|
||||
ghost_coarse = ((gy >> 1) * nx + (gx >> 1)).astype(np.uint32)
|
||||
targets, children = [], []
|
||||
ry, rx = np.nonzero(_dilate(covered, 1) & ~covered)
|
||||
for y, x in zip(ry.tolist(), rx.tolist()):
|
||||
for q in range(1, 9):
|
||||
if _closed_union_contains(covered, 2 * x + 1 - D2Q9_EX[q], 2 * y + 1 - D2Q9_EY[q]):
|
||||
targets.append(q * ny * nx + y * nx + x)
|
||||
children.append([q * fy * fx + (2 * y + dy) * fx + 2 * x + dx
|
||||
for dy in (0, 1) for dx in (0, 1)])
|
||||
targets = np.asarray(targets, np.uint64)
|
||||
children = np.asarray(children, np.uint64).reshape(-1, 4)
|
||||
cells = np.asarray(children % np.uint64(fy * fx), np.intp)
|
||||
if not (kind.ravel()[cells] == INNER_GHOST).all():
|
||||
raise ValueError("coalesce children must be INNER ghost cells")
|
||||
pair = cls((ny, nx), tuple(patches), patches[0].tau_coarse, patches[0].tau_fine,
|
||||
_readonly(kind, np.uint8), _readonly(ghost_fine, np.uint64),
|
||||
_readonly(ghost_coarse, np.uint32), _readonly(targets, np.uint64),
|
||||
_readonly(children, np.uint64))
|
||||
_transfer_footprint(pair)
|
||||
_check_parent_pull_closure(pair)
|
||||
return pair
|
||||
ghost_fine, coalesce_fine = [], []
|
||||
for p in patches:
|
||||
y0, x0 = p.fine_level_origin
|
||||
ly, lx = p.fine_shape
|
||||
kind[y0:y0 + ly, x0:x0 + lx] = p.fine_kind
|
||||
gy, gx = np.divmod(p.ghost_fine.astype(np.int64), lx)
|
||||
ghost_fine.append((gy + y0) * fx + gx + x0)
|
||||
q, cell = np.divmod(p.coalesce_fine.astype(np.int64), ly * lx)
|
||||
cy, cx = np.divmod(cell, lx)
|
||||
coalesce_fine.append(q * fy * fx + (cy + y0) * fx + cx + x0)
|
||||
ghost_coarse = np.concatenate([p.ghost_coarse for p in patches])
|
||||
coalesce_coarse = np.concatenate([p.coalesce_coarse for p in patches])
|
||||
covered = np.zeros((ny, nx), bool)
|
||||
for p in patches:
|
||||
covered |= p.covered
|
||||
cells = ny * nx
|
||||
if (covered.ravel()[ghost_coarse].any()
|
||||
or covered.ravel()[coalesce_coarse % np.uint64(cells)].any()
|
||||
or len(np.unique(coalesce_coarse)) != len(coalesce_coarse)):
|
||||
raise ValueError("patch transfers overlap another patch")
|
||||
return cls((ny, nx), tuple(patches), patches[0].tau_coarse, patches[0].tau_fine,
|
||||
_readonly(kind, np.uint8), _readonly(np.concatenate(ghost_fine), np.uint64),
|
||||
_readonly(ghost_coarse, np.uint32), _readonly(coalesce_coarse, np.uint64),
|
||||
_readonly(np.concatenate(coalesce_fine), np.uint64).reshape(-1, 4))
|
||||
|
||||
@property
|
||||
def fine_shape(self) -> tuple[int, int]:
|
||||
@@ -230,18 +227,11 @@ class FixedPatchSet2D:
|
||||
def bounds(self) -> tuple:
|
||||
return tuple(p.bounds for p in self.patches)
|
||||
|
||||
rectangles = bounds
|
||||
|
||||
@property
|
||||
def fine_origin(self) -> tuple[float, float]:
|
||||
"""Lower edge of the level-wide fine grid in coarse physical units."""
|
||||
return 0.0, 0.0
|
||||
|
||||
@property
|
||||
def fine_level_offset(self) -> tuple[int, int]:
|
||||
"""The level-wide grid itself: zero (y, x) index offset."""
|
||||
return 0, 0
|
||||
|
||||
@property
|
||||
def covered(self) -> np.ndarray:
|
||||
mask = np.zeros(self.coarse_shape, bool)
|
||||
@@ -250,61 +240,22 @@ class FixedPatchSet2D:
|
||||
return mask
|
||||
|
||||
def fine_layout(self, block):
|
||||
"""Sparse non-periodic tiles covering union support; domain divisibility remains."""
|
||||
"""Sparse non-periodic layout storing exactly the patch supports."""
|
||||
from .blocks import BlockLayout2D
|
||||
block = tuple(block)
|
||||
if len(block) != 2 or any(isinstance(v, (bool, np.bool_)) or not isinstance(v, Integral)
|
||||
or v <= 0 for v in block):
|
||||
raise ValueError("fine block must be two positive integers (by, bx)")
|
||||
by, bx = map(int, block)
|
||||
support = _dilate(self.fine_kind == ACTIVE, HALO)
|
||||
ty, tx = np.nonzero(support)
|
||||
origins = sorted({(int(y) // by * by, int(x) // bx * bx) for y, x in zip(ty, tx)})
|
||||
origins = set()
|
||||
for p in self.patches:
|
||||
y0, x0 = p.fine_level_origin
|
||||
ly, lx = p.fine_shape
|
||||
if y0 % by or x0 % bx or ly % by or lx % bx:
|
||||
raise ValueError(f"patch {p.bounds} support is not aligned to fine blocks {block}")
|
||||
origins.update((y, x) for y in range(y0, y0 + ly, by) for x in range(x0, x0 + lx, bx))
|
||||
return BlockLayout2D.build(*self.fine_shape, by, bx, periodic=(False, False),
|
||||
origins=origins)
|
||||
|
||||
|
||||
def _dilate(mask, steps):
|
||||
"""Chebyshev dilation with a zero (non-periodic) exterior."""
|
||||
out = np.asarray(mask, bool)
|
||||
for _ in range(steps):
|
||||
grown = out.copy()
|
||||
grown[1:] |= out[:-1]
|
||||
grown[:-1] |= out[1:]
|
||||
wide = grown.copy()
|
||||
wide[:, 1:] |= grown[:, :-1]
|
||||
wide[:, :-1] |= grown[:, 1:]
|
||||
out = wide
|
||||
return out
|
||||
|
||||
|
||||
def _closed_union_contains(covered, mx, my):
|
||||
"""Does doubled point (mx, my) lie in a closed covered square [2X, 2X+2] x [2Y, 2Y+2]?"""
|
||||
xs = (mx // 2 - 1, mx // 2) if mx % 2 == 0 else ((mx - 1) // 2,)
|
||||
ys = (my // 2 - 1, my // 2) if my % 2 == 0 else ((my - 1) // 2,)
|
||||
ny, nx = covered.shape
|
||||
return any(0 <= y < ny and 0 <= x < nx and covered[y, x] for y in ys for x in xs)
|
||||
|
||||
|
||||
def _check_parent_pull_closure(pair):
|
||||
"""Every uncovered parent cell pulling q from a covered cell needs its (R, q) row; no row
|
||||
may stage into an uncovered parent cell unless its link passes the closed union."""
|
||||
ny, nx = pair.coarse_shape
|
||||
rows = set(map(int, pair.coalesce_coarse))
|
||||
if len(rows) != len(pair.coalesce_coarse):
|
||||
raise ValueError("duplicate coalesce writer (receiver, q)")
|
||||
covered = pair.covered
|
||||
for y, x in zip(*np.nonzero(~covered)):
|
||||
for q in range(1, 9):
|
||||
sy, sx = y - D2Q9_EY[q], x - D2Q9_EX[q]
|
||||
if 0 <= sy < ny and 0 <= sx < nx and covered[sy, sx]:
|
||||
if q * ny * nx + y * nx + x not in rows:
|
||||
raise ValueError("a surviving parent ACTIVE pull lacks its coalesce row")
|
||||
for slot in rows:
|
||||
q, cell = divmod(slot, ny * nx)
|
||||
y, x = divmod(cell, nx)
|
||||
if covered[y, x] or not _closed_union_contains(covered, 2*x + 1 - D2Q9_EX[q], 2*y + 1 - D2Q9_EY[q]):
|
||||
raise ValueError("coalesce row outside the union boundary")
|
||||
origins=sorted(origins))
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
@@ -372,14 +323,14 @@ def nest_patch(cfg, parent, child_bounds):
|
||||
|
||||
|
||||
def nest_patch_set(cfg, parent, child_rectangles_in_parent):
|
||||
"""Child rectangle union in the parent's allocated L1 array coordinates.
|
||||
"""Separated child union in the parent's allocated L1 array coordinates.
|
||||
|
||||
The root edge coordinate of L1 index (x, y) is
|
||||
``parent.fine_origin + (x, y)/2``. For a set parent its origin is zero;
|
||||
a scalar parent's window includes its ghost band and has a shifted origin.
|
||||
The child set allocates a dense public extent twice that L1 array, while
|
||||
its blocked storage omits holes. Validate the actual adjacent-pair support over the whole union;
|
||||
no rectangle-gap policy is imposed.
|
||||
its blocked storage omits holes. Both adjacent pairs retain gap >= 4 in
|
||||
their respective parent-cell units. Validate support over the whole union.
|
||||
"""
|
||||
child = FixedPatchSet2D.from_config(
|
||||
_middle_config(cfg, parent), child_rectangles_in_parent)
|
||||
@@ -422,19 +373,8 @@ def _nested_kind(parent, child):
|
||||
return _readonly(composed, np.uint8)
|
||||
|
||||
|
||||
def _circle_in_coverage(covered, cx, cy, radius):
|
||||
"""Exact disk versus uncovered parent squares; no containing-rectangle rule."""
|
||||
ny, nx = covered.shape
|
||||
if cx-radius < 0 or cy-radius < 0 or cx+radius > nx or cy+radius > ny:
|
||||
return False
|
||||
y, x = np.nonzero(~covered)
|
||||
dx = np.maximum(np.maximum(x-cx, cx-(x+1)), 0.)
|
||||
dy = np.maximum(np.maximum(y-cy, cy-(y+1)), 0.)
|
||||
return not np.any(dx*dx + dy*dy < radius*radius)
|
||||
|
||||
|
||||
def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
parent_origin=(0., 0.), parent_spacing=1., _shifted_blocked=False):
|
||||
parent_origin=(0., 0.), parent_spacing=1.):
|
||||
"""Project fixed circles and reject boundary/transfer overlap before CUDA.
|
||||
|
||||
Input centers/radii and Jets are in the root edge frame. ``parent_origin``
|
||||
@@ -446,7 +386,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
Original objects are not retained or mutated. No runtime geometry updates.
|
||||
``patch`` may be a FixedPatchSet2D: objects are then projected once onto
|
||||
the level-wide fine grid (fine_origin (0, 0)); each circle must lie inside
|
||||
the union, body ids are the positions in ``objects`` (never patch ids), and
|
||||
one patch, body ids are the positions in ``objects`` (never patch ids), and
|
||||
jets and the (receiver, dir) writer checks run once over the whole level.
|
||||
``jets`` use the same coarse cell-edge frame as the objects; their
|
||||
selection is checked against true fine hits here. Jet amplitudes need no
|
||||
@@ -463,8 +403,8 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (patch.coarse_shape != (cfg.ny, cfg.nx) or not checked.is_d2q9
|
||||
or checked.collision not in ("SRT", "MRT") or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32" or checked.les_enabled
|
||||
or checked.collision != "SRT" or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32" or checked.ddf_shifting or checked.les_enabled
|
||||
or patch.tau_coarse != 3*checked.viscosity+.5):
|
||||
raise ValueError("Fine boundary requires the matching fixed-patch configuration")
|
||||
from ..body.geometry.circle import CircleGeometry
|
||||
@@ -476,6 +416,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
flags = np.full(nx * ny, FLUID, np.uint16)
|
||||
active = patch.fine_kind.ravel() == ACTIVE
|
||||
local, links = [], []
|
||||
rects = patch.bounds if isinstance(patch, FixedPatchSet2D) else (patch.bounds,)
|
||||
covered = patch.covered
|
||||
dx = dt = .5 * parent_spacing
|
||||
fine_origin = origin + parent_spacing * np.asarray(patch.fine_origin)
|
||||
@@ -489,8 +430,9 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
px, py = (np.asarray((cx, cy)) - origin) / parent_spacing
|
||||
pr = radius / parent_spacing
|
||||
if (not np.isfinite([cx, cy, radius]).all() or radius <= 0
|
||||
or not _circle_in_coverage(covered, px, py, pr)):
|
||||
raise ValueError("Entire fine circle must lie inside the physical patch union")
|
||||
or not any(x0 <= px-pr and px+pr <= x1 and y0 <= py-pr
|
||||
and py+pr <= y1 for x0, y0, x1, y1 in rects)):
|
||||
raise ValueError("Entire fine circle must lie inside one physical patch")
|
||||
center = tuple((v-o)/dx-.5 for v, o in zip((cx, cy), fine_origin))
|
||||
spec = obj.surface_actuation
|
||||
if spec is not None:
|
||||
@@ -567,7 +509,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
jet_cfg.validate()
|
||||
fluid_idx, dirs, _, body_id, rx, ry, _, _ = packed
|
||||
surface_link_velocity(local, jet_cfg, flags, fluid_idx, dirs, body_id, rx, ry,
|
||||
jets=jets, hits=hits, _shifted_blocked=_shifted_blocked)
|
||||
jets=jets, hits=hits)
|
||||
return FineBoundary2D(tuple(local), _readonly(flags, np.uint16),
|
||||
tuple(_readonly(a, a.dtype) for a in packed),
|
||||
_readonly(sorted(boundary), np.uint32),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user