Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26e49451ec | ||
|
|
e214480452 | ||
|
|
c414f6c7f6 | ||
|
|
decd419e94 | ||
|
|
1c47cb8570 | ||
|
|
ee7074a53d | ||
|
|
b97fc79af9 | ||
|
|
8e9fc72e19 | ||
|
|
d2cd1114ed | ||
|
|
c95a7baae4 |
No files matched your search
@@ -0,0 +1,87 @@
|
|||||||
|
# Supported settings: 2D Jet + fixed multilevel refinement (candidate c95a7ba)
|
||||||
|
|
||||||
|
Every entry names the guard that enforces it (file:line at c95a7ba) 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:65`, `lbm/kernels/blocks.cu:25`, `lbm/kernels/refinement.cu:26` |
|
||||||
|
| Precision / streaming | FP32 storage, double buffer | same guards |
|
||||||
|
| Collision on refinement levels | SRT or MRT; TRT and LES rejected | `lbm/refinement.py:65` (re-checked for fine boundaries at `lbm/refinement.py:457`) |
|
||||||
|
| Shifted (ddf_shifting) storage | only when every level is blocked; dense or mixed rejected | `lbm/refinement_stepper.py:198`, `:469`; `lbm/kernels/refinement.cu:26` (dense patch kernel unshifted only) |
|
||||||
|
| Fine relaxation | 1/tau_fine must lie inside the configured omega guards | `lbm/refinement.py:80` |
|
||||||
|
| Uniform blocked backend `PeriodicBlockStepper` | doubly periodic, SRT only, stationary passive circles | `lbm/block_stepper.py:84`, `:91`, `:45`-`:67` |
|
||||||
|
|
||||||
|
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:72`, `:75` |
|
||||||
|
| Single L1 rectangle | two-coarse-cell margin to the domain edge | `lbm/refinement.py:73` |
|
||||||
|
| 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:84`; `lbm/refinement.py:283`-`:295` |
|
||||||
|
| 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:82`, `:87`; `lbm/hierarchy.py:54` |
|
||||||
|
| Nested transfer support | child only over inherited ACTIVE cells, donors/receivers uncovered ACTIVE, no child over the parent restriction footprint | `lbm/refinement.py:391`-`:407` |
|
||||||
|
| Tile shape | positive `(by, bx)`; the level allocation must be divisible by the tile (no partial tiles yet) | `lbm/refinement_stepper.py:78`, `lbm/refinement.py:246` (shape); `lbm/blocks.py:134` (divisibility) |
|
||||||
|
| Coarse boundary | `periodic`, `slot_reservoir` or `channel` | `lbm/refinement_stepper.py:177` |
|
||||||
|
| `slot_reservoir` | on-site velocity bottom slot, reservoir top, velocity 0, 2 <= slot width < nx, patch rows 4..ny-4 | `lbm/refinement_stepper.py:205`, `:212`; `lbm/refinement.py:546` |
|
||||||
|
| `channel` | all levels blocked; Zou-He inlet, pressure-NEQ outlet, stationary bounce-back walls; transfers away from the closure | `lbm/channel.py:22`, `:26`, `:43`, `:54` |
|
||||||
|
| Explosion | `constant` (default); `projected_linear` only periodic, all-blocked SRT, no bodies/Jets, no LES (experimental) | `lbm/refinement_stepper.py:188`, `:193` |
|
||||||
|
| L2 runtime remap (`remap_child`) | periodic three-level L2 sets only, no L0/L1 bodies, identical pinned boundary | `lbm/refinement_stepper.py:557`, `:567`-`:581` |
|
||||||
|
|
||||||
|
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:474`-`:542` |
|
||||||
|
| Level | not on L0; only on the finest level of a nested hierarchy | `lbm/refinement_stepper.py:80`, `:89` |
|
||||||
|
| Jet condition | velocity only; `pressure` is reserved and raises NotImplementedError | `body/jets.py:148`, `:150` |
|
||||||
|
| 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:155`, `:175`, `:177`, `:255`, `:261`, `:263` |
|
||||||
|
| Jets with shifted storage | only in the all-blocked stepper with SRT and no LES | `body/jets.py:249` |
|
||||||
|
| SurfaceActuation | unshifted storage only | `body/objects.py:155` |
|
||||||
|
| Raw boundary exchange (force) | blocked body levels only; the window ledger requires `observe_raw_exchange` | `lbm/refinement_stepper.py:264`, `:269` |
|
||||||
|
| Checkpoint with Jets | refused | `simulation.py:687` |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Not supported in this release
|
||||||
|
|
||||||
|
3D, dynamic/automatic AMR (tagging, time-varying hierarchies beyond `remap_child`),
|
||||||
|
moving bodies on refined levels, pressure Jets, Jet checkpoints, LES or TRT on refined
|
||||||
|
levels, partial (non-dividing) tiles.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Checkpoint save -> load round trip for a scene with passive bodies. Requires GPU.
|
||||||
|
|
||||||
|
A fresh Simulation with the same scene, restored from the checkpoint must hold the same populations,
|
||||||
|
flags, body count and step count, and must stay bitwise identical to the original
|
||||||
|
after both continue for the same number of steps. Checkpoints with Jets are
|
||||||
|
refused (documented limitation) and are not covered here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ._cases import make_sim
|
||||||
|
|
||||||
|
# Three separated cylinders (pinball-like); cut-link receivers stay in bulk fluid.
|
||||||
|
CIRCLES = (((40, 32), 6), ((56, 22), 6), ((56, 42), 6))
|
||||||
|
|
||||||
|
|
||||||
|
def scene():
|
||||||
|
sim = make_sim()
|
||||||
|
sim.initialize()
|
||||||
|
for center, radius in CIRCLES:
|
||||||
|
sim.add_body("circle", center=center, radius=radius)
|
||||||
|
sim.sync_bodies()
|
||||||
|
return sim
|
||||||
|
|
||||||
|
|
||||||
|
class CheckpointBodiesGPU(unittest.TestCase):
|
||||||
|
def test_round_trip_is_bitwise_and_continues_identically(self):
|
||||||
|
sim = scene()
|
||||||
|
sim.run(60)
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
restored = None
|
||||||
|
try:
|
||||||
|
saved = sim.save_checkpoint(path)
|
||||||
|
# The loader requires the same body count and geometry: rebuild the scene.
|
||||||
|
restored = scene()
|
||||||
|
restored.load_checkpoint(saved)
|
||||||
|
|
||||||
|
self.assertEqual(restored.bodies.count, len(CIRCLES))
|
||||||
|
self.assertEqual(restored.stepper.step_count, sim.stepper.step_count)
|
||||||
|
np.testing.assert_array_equal(restored.get_flags(), sim.get_flags())
|
||||||
|
a, b = sim.get_ddf(), restored.get_ddf()
|
||||||
|
self.assertEqual(a.dtype, b.dtype)
|
||||||
|
self.assertEqual(a.tobytes(), b.tobytes())
|
||||||
|
|
||||||
|
sim.run(40)
|
||||||
|
restored.run(40)
|
||||||
|
self.assertEqual(restored.stepper.step_count, sim.stepper.step_count)
|
||||||
|
a, b = sim.get_ddf(), restored.get_ddf()
|
||||||
|
self.assertTrue(np.isfinite(a).all())
|
||||||
|
self.assertEqual(a.tobytes(), b.tobytes())
|
||||||
|
for i in range(len(CIRCLES)):
|
||||||
|
self.assertTrue(np.all(np.isfinite(restored.read_force(i))))
|
||||||
|
finally:
|
||||||
|
if restored is not None:
|
||||||
|
restored.close()
|
||||||
|
os.unlink(path)
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -50,7 +50,7 @@ def test_masks_and_minimal_footprint():
|
|||||||
def test_actual_closure_read_conflicts():
|
def test_actual_closure_read_conflicts():
|
||||||
pair, layout, targets = transfer((6, 6, 18, 14))
|
pair, layout, targets = transfer((6, 6, 18, 14))
|
||||||
validate_channel_transfers(pair, layout, targets)
|
validate_channel_transfers(pair, layout, targets)
|
||||||
for target in (layout.slot(layout.owner[5, -1]-1, 2),
|
for target in (layout.slot(int(layout.owner[5, -1]) - 1, 2), # integer under NumPy 1 and 2
|
||||||
layout.slot(layout.owner[1, 4], 4),
|
layout.slot(layout.owner[1, 4], 4),
|
||||||
layout.slot(layout.owner[-2, 4], 3)):
|
layout.slot(layout.owner[-2, 4], 3)):
|
||||||
with pytest.raises(ValueError, match="immutable"):
|
with pytest.raises(ValueError, match="immutable"):
|
||||||
|
|||||||
@@ -141,16 +141,47 @@ def test_axis_specific_sampling_phase():
|
|||||||
np.testing.assert_allclose(fine_y - ((k + 43) - 0.5) * 0.00125, 0, atol=1e-15)
|
np.testing.assert_allclose(fine_y - ((k + 43) - 0.5) * 0.00125, 0, atol=1e-15)
|
||||||
|
|
||||||
|
|
||||||
|
def frozen_manifest(observation_mode):
|
||||||
|
"""Frozen MRT manifest JSON as recorded, without hashing the working tree."""
|
||||||
|
name = "source_manifest_window.json" if observation_mode == "window" else "source_manifest_mrt.json"
|
||||||
|
return json.loads((r.HERE / "data/refined_dfg" / name).read_text())
|
||||||
|
|
||||||
|
|
||||||
|
def selected_manifest(collision, observation_mode):
|
||||||
|
"""verify_source's manifest selection with its working-tree hash comparison neutralised.
|
||||||
|
|
||||||
|
Source identity is owned by test_source_pin; this exercises only which frozen manifest
|
||||||
|
verify_source selects and returns for (collision, observation_mode).
|
||||||
|
"""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
class Digest(str):
|
||||||
|
def __ne__(self, other):
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Hash:
|
||||||
|
def __init__(self, data=b""):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def hexdigest(self):
|
||||||
|
return Digest()
|
||||||
|
|
||||||
|
with patch.object(r.hashlib, "sha256", Hash):
|
||||||
|
return r.verify_source(r.DEFAULT_SOURCE, collision, observation_mode)
|
||||||
|
|
||||||
|
|
||||||
def test_full_requires_completed_pilot():
|
def test_full_requires_completed_pilot():
|
||||||
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
||||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
|
r.verify_pilot(None, frozen_manifest("window"))
|
||||||
|
|
||||||
|
|
||||||
def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"):
|
def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"):
|
||||||
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
||||||
|
# Receipt-logic fixture: the frozen manifest verify_source returns for a matching tree.
|
||||||
|
# Source identity itself is checked only by test_source_pin.
|
||||||
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario)
|
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario)
|
||||||
if observation_mode == "window" else
|
if observation_mode == "window" and jet_scenario != "none" else
|
||||||
json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text()))
|
frozen_manifest(observation_mode))
|
||||||
selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement)
|
selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement)
|
||||||
(path / "plan.json").write_text(
|
(path / "plan.json").write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@@ -348,7 +379,7 @@ def test_record_actual_checkout_separately():
|
|||||||
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
||||||
assert len(actual["checkout_revision"]) == 40
|
assert len(actual["checkout_revision"]) == 40
|
||||||
assert isinstance(actual["checkout_dirty"], bool)
|
assert isinstance(actual["checkout_dirty"], bool)
|
||||||
assert r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")["production_origin_commit"].startswith("d930")
|
assert selected_manifest("MRT", "window")["production_origin_commit"].startswith("d930")
|
||||||
|
|
||||||
|
|
||||||
def test_window_plan_preserves_physics():
|
def test_window_plan_preserves_physics():
|
||||||
@@ -358,7 +389,7 @@ def test_window_plan_preserves_physics():
|
|||||||
assert baseline.pop("observation_mode") == "last_base_step"
|
assert baseline.pop("observation_mode") == "last_base_step"
|
||||||
assert window.pop("observation_mode") == "window"
|
assert window.pop("observation_mode") == "window"
|
||||||
assert baseline == window
|
assert baseline == window
|
||||||
assert r.verify_source(r.DEFAULT_SOURCE, collision, "window")["observation_mode"] == "window"
|
assert selected_manifest(collision, "window")["observation_mode"] == "window"
|
||||||
with unittest.TestCase().assertRaises(ValueError):
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
|
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
|
||||||
|
|
||||||
|
|||||||
@@ -78,13 +78,15 @@ class ShiftedStorage(unittest.TestCase):
|
|||||||
|
|
||||||
def test_shifted_density_and_tag_validation(self):
|
def test_shifted_density_and_tag_validation(self):
|
||||||
old,new = topology(A),topology(B)
|
old,new = topology(A),topology(B)
|
||||||
a = tuple(np.where(m[None], np.float32(-.001), np.nan).repeat(9,axis=0) for m in old.leaf_masks)
|
a = tuple(np.where(m[None], np.float32(-.001), np.float32(np.nan)).repeat(9,axis=0)
|
||||||
|
for m in old.leaf_masks) # FP32 under NumPy 1 and 2 promotion rules
|
||||||
remap(old,new,a)
|
remap(old,new,a)
|
||||||
with self.assertRaisesRegex(ValueError,'representation'): remap(old,new,a,False)
|
with self.assertRaisesRegex(ValueError,'representation'): remap(old,new,a,False)
|
||||||
with self.assertRaisesRegex(ValueError,'FP32'): remap(old,new,tuple(x.astype(np.float64) for x in a))
|
with self.assertRaisesRegex(ValueError,'FP32'): remap(old,new,tuple(x.astype(np.float64) for x in a))
|
||||||
a[0][:,old.leaf_masks[0]] = -1
|
a[0][:,old.leaf_masks[0]] = -1
|
||||||
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
|
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
|
||||||
rest = tuple(np.where(m[None],np.float32(0),np.nan).repeat(9,axis=0) for m in old.leaf_masks)
|
rest = tuple(np.where(m[None],np.float32(0),np.float32(np.nan)).repeat(9,axis=0)
|
||||||
|
for m in old.leaf_masks)
|
||||||
remap(old,new,rest)
|
remap(old,new,rest)
|
||||||
with self.assertRaisesRegex(ValueError,'positive density'):
|
with self.assertRaisesRegex(ValueError,'positive density'):
|
||||||
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
|
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ class CpuContracts(unittest.TestCase):
|
|||||||
backend._lock = RLock()
|
backend._lock = RLock()
|
||||||
backend._status = "ready"
|
backend._status = "ready"
|
||||||
backend._step_count = 0
|
backend._step_count = 0
|
||||||
|
backend._ddf_shifting = False # unshifted storage, as for the default config
|
||||||
backend._stream = Mock()
|
backend._stream = Mock()
|
||||||
backend._module = Mock()
|
backend._module = Mock()
|
||||||
backend._copy = backend._step = Mock()
|
backend._copy = backend._step = Mock()
|
||||||
|
|||||||
@@ -282,7 +282,8 @@ def test_fine_surface_four_hops(speed):
|
|||||||
@pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"])
|
@pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"])
|
||||||
def test_missing_surface_device_buffer_rejects_before_launch(missing):
|
def test_missing_surface_device_buffer_rejects_before_launch(missing):
|
||||||
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
|
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
|
||||||
backend.fine_boundary = SimpleNamespace(objects=[object_spec(.002)])
|
backend._boundary_hosts = [None, SimpleNamespace(objects=[object_spec(.002)])]
|
||||||
|
backend._level_jets = [(), ()]
|
||||||
backend._fine_curved = SimpleNamespace(fluid_idx_gpu=object(),
|
backend._fine_curved = SimpleNamespace(fluid_idx_gpu=object(),
|
||||||
surface_velocity_gpu=object(), link_exchange_gpu=object())
|
surface_velocity_gpu=object(), link_exchange_gpu=object())
|
||||||
setattr(backend._fine_curved, missing, None)
|
setattr(backend._fine_curved, missing, None)
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ class FaultGuardsCPU(unittest.TestCase):
|
|||||||
|
|
||||||
def fake_backend(self):
|
def fake_backend(self):
|
||||||
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
|
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
|
||||||
backend.fine_layout = None # dense fine storage
|
backend._layouts = [None, None] # dense storage on both levels
|
||||||
|
backend._ddf_shifting = False # unshifted on every level, as _check_storage expects
|
||||||
|
backend._level_cfgs = [LBMConfig(nx=16, ny=16, viscosity=.1)] * 2 # ddf_shifting off
|
||||||
backend.ctx = Mock(activate=nullcontext)
|
backend.ctx = Mock(activate=nullcontext)
|
||||||
backend._lock = RLock()
|
backend._lock = RLock()
|
||||||
backend._status = "ready"
|
backend._status = "ready"
|
||||||
@@ -158,10 +160,21 @@ class FaultGuardsCPU(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(RuntimeError, "four-cell"):
|
with self.assertRaisesRegex(RuntimeError, "four-cell"):
|
||||||
PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)])
|
PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)])
|
||||||
p = implementation.FixedPatch2D.from_config(cfg, (4, 4, 12, 12))
|
p = implementation.FixedPatch2D.from_config(cfg, (4, 4, 12, 12))
|
||||||
with patch.object(cuda, "Stream"), patch.object(implementation.FixedPatch2D, "from_config",
|
from CelerisLab.lbm import hierarchy
|
||||||
return_value=replace(p, tau_coarse=.9)):
|
bad = patch.object(implementation.FixedPatch2D, "from_config",
|
||||||
with self.assertRaisesRegex(RuntimeError, "omega/tau"):
|
return_value=replace(p, tau_coarse=.9))
|
||||||
|
# A pair tau inconsistent with its level config is rejected twice over, both before
|
||||||
|
# any device allocation or upload: first by the hierarchy chain check, and, if that
|
||||||
|
# is bypassed, by the per-level omega/tau check in _build_level.
|
||||||
|
with patch.object(cuda, "Stream"), bad, patch.object(cuda, "mem_alloc") as alloc, \
|
||||||
|
patch.object(cuda, "memcpy_htod") as upload:
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "config chain is inconsistent"):
|
||||||
PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds])
|
PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds])
|
||||||
|
with patch.object(hierarchy, "_check_chain"), \
|
||||||
|
self.assertRaisesRegex(RuntimeError, "omega/tau"):
|
||||||
|
PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds])
|
||||||
|
alloc.assert_not_called()
|
||||||
|
upload.assert_not_called()
|
||||||
module = Mock()
|
module = Mock()
|
||||||
module.get_global.return_value = (Mock(), 16)
|
module.get_global.return_value = (Mock(), 16)
|
||||||
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
||||||
|
|||||||
@@ -249,19 +249,35 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
self.assertFalse(assess(incomplete)["comparisons"][3]["passed"])
|
self.assertFalse(assess(incomplete)["comparisons"][3]["passed"])
|
||||||
|
|
||||||
def test_pinned_build_sources_match_host_generated_inputs(self):
|
def test_pinned_build_sources_match_host_generated_inputs(self):
|
||||||
|
"""The pinned fingerprints are what today's compiler generates from the qualified
|
||||||
|
NUMERICAL_REVISION kernels; the current kernels may legitimately differ."""
|
||||||
|
import tarfile
|
||||||
from CelerisLab.cuda import compiler_v2
|
from CelerisLab.cuda import compiler_v2
|
||||||
|
|
||||||
for size in (128, 256):
|
kernels = "src/CelerisLab/lbm/kernels"
|
||||||
cfg = replace(run_case.config(size, 100), nx=10, ny=10)
|
if subprocess.run(["git", "-C", str(REPO), "cat-file", "-e",
|
||||||
cfg.validate()
|
f"{NUMERICAL_REVISION}^{{commit}}"], capture_output=True).returncode:
|
||||||
root = compiler_v2.generate_config(cfg)
|
self.fail(f"{NUMERICAL_REVISION} is not in this clone; the pinned build "
|
||||||
try:
|
"sources cannot be checked")
|
||||||
actual = json.loads((Path(root) / ".celeris-build").read_text())
|
archive = subprocess.run(["git", "-C", str(REPO), "archive", "--format=tar",
|
||||||
expected = build_metadata(size)
|
NUMERICAL_REVISION, kernels],
|
||||||
self.assertEqual(actual, {key: expected[key] for key in
|
capture_output=True, check=True).stdout
|
||||||
("source_sha256", "config", "n_objects")})
|
with tempfile.TemporaryDirectory() as snapshot:
|
||||||
finally:
|
with tarfile.open(fileobj=io.BytesIO(archive)) as tar:
|
||||||
compiler_v2.cleanup_build_dir(root)
|
tar.extractall(snapshot)
|
||||||
|
with patch.object(compiler_v2, "_KERNEL_DIR", str(Path(snapshot) / kernels)):
|
||||||
|
for size in (128, 256):
|
||||||
|
with self.subTest(size=size):
|
||||||
|
cfg = replace(run_case.config(size, 100), nx=10, ny=10)
|
||||||
|
cfg.validate()
|
||||||
|
root = compiler_v2.generate_config(cfg)
|
||||||
|
try:
|
||||||
|
actual = json.loads((Path(root) / ".celeris-build").read_text())
|
||||||
|
expected = build_metadata(size)
|
||||||
|
self.assertEqual(actual, {key: expected[key] for key in
|
||||||
|
("source_sha256", "config", "n_objects")})
|
||||||
|
finally:
|
||||||
|
compiler_v2.cleanup_build_dir(root)
|
||||||
|
|
||||||
def test_source_preflight_requires_clean_exact_revision_and_tree(self):
|
def test_source_preflight_requires_clean_exact_revision_and_tree(self):
|
||||||
import oracle
|
import oracle
|
||||||
|
|||||||
Reference in new issue
Block a user