Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51690fa898 | ||
|
|
11cbf1194f | ||
|
|
7afb5addde |
+1
-1
@@ -7,7 +7,7 @@ it is; it adds no framework. Line references are to candidate 0f8fdbc3.
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 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`. |
|
| 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. Uniform grid: rebuilt by body sync. `FixedPatchStepper`: a geometry change is a new instance (`remap_child`), committed only after a successful transfer. | No in-place table edits. |
|
| 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. Uniform grid: rebuilt by body sync. `FixedPatchStepper`: a geometry change is a new instance (`remap_child`), 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`). | Host copy commits only after the device upload succeeded; a device error fails the instance. |
|
| 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 call except `close()` raises, `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. |
|
| 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:
|
Storage routes are separate concerns:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class Simulation:
|
|||||||
# Internal stream for async DTOH coordination
|
# Internal stream for async DTOH coordination
|
||||||
self._stream: Optional[cuda.Stream] = None
|
self._stream: Optional[cuda.Stream] = None
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
self._failed = None # reason after an uncertain device write; only close() remains
|
||||||
|
|
||||||
# Load configs
|
# Load configs
|
||||||
self.lbm_cfg = load_lbm_config(lbm_config_path)
|
self.lbm_cfg = load_lbm_config(lbm_config_path)
|
||||||
@@ -172,15 +173,26 @@ class Simulation:
|
|||||||
def set_jet_amplitude(self, index: int, amplitude: float) -> None:
|
def set_jet_amplitude(self, index: int, amplitude: float) -> None:
|
||||||
"""Change one jet's signed amplitude; re-uploads per-link velocity only.
|
"""Change one jet's signed amplitude; re-uploads per-link velocity only.
|
||||||
|
|
||||||
Invalid input changes nothing. A device error leaves the host amplitude
|
Invalid input raises before any change. Any error after validation
|
||||||
unchanged but the device column undefined: treat it like any other CUDA
|
leaves the host amplitude unchanged but the device column undefined,
|
||||||
exception here and close/recreate the Simulation before stepping again.
|
so the Simulation is marked failed: every later call except close()
|
||||||
|
raises; close and recreate it.
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
jet = self.bodies.jets[index].with_amplitude(amplitude)
|
jet = self.bodies.jets[index].with_amplitude(amplitude)
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
self.field.curved.set_jet_amplitude(index, amplitude, self.stream)
|
try:
|
||||||
|
self.field.curved.set_jet_amplitude(index, amplitude, self.stream)
|
||||||
|
except BaseException as exc:
|
||||||
|
self._failed = f"jet amplitude upload failed: {exc!r}"
|
||||||
|
raise
|
||||||
self.bodies.jets[index] = jet # later geometry syncs keep the new amplitude
|
self.bodies.jets[index] = jet # later geometry syncs keep the new amplitude
|
||||||
|
|
||||||
|
def _require_usable(self):
|
||||||
|
failed = getattr(self, "_failed", None)
|
||||||
|
if failed is not None:
|
||||||
|
raise RuntimeError(f"Simulation failed ({failed}); close() and recreate it")
|
||||||
|
|
||||||
def remove_body(self, id: int) -> None:
|
def remove_body(self, id: int) -> None:
|
||||||
"""Remove a body from the simulation.
|
"""Remove a body from the simulation.
|
||||||
|
|
||||||
@@ -338,6 +350,7 @@ class Simulation:
|
|||||||
Unlike read_body this is not accumulated/time-averaged. It resets after
|
Unlike read_body this is not accumulated/time-averaged. It resets after
|
||||||
geometry sync or checkpoint load; advance before interpreting it.
|
geometry sync or checkpoint load; advance before interpreting it.
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
obj = self.bodies.get(id)
|
obj = self.bodies.get(id)
|
||||||
curved = self.field.curved
|
curved = self.field.curved
|
||||||
has_aperture = (curved.aperture is not None
|
has_aperture = (curved.aperture is not None
|
||||||
@@ -365,6 +378,7 @@ class Simulation:
|
|||||||
BodyTelemetry with fields ``force``, ``torque``, ``sensor``.
|
BodyTelemetry with fields ``force``, ``torque``, ``sensor``.
|
||||||
``sensor`` is a dim-length array of zeros for non-sensor bodies.
|
``sensor`` is a dim-length array of zeros for non-sensor bodies.
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
if self.bodies.obs_pinned is None:
|
if self.bodies.obs_pinned is None:
|
||||||
raise RuntimeError("No obs buffer. Call run() first.")
|
raise RuntimeError("No obs buffer. Call run() first.")
|
||||||
if stream is None:
|
if stream is None:
|
||||||
@@ -384,6 +398,7 @@ class Simulation:
|
|||||||
This is the most efficient readback for DRL control loops
|
This is the most efficient readback for DRL control loops
|
||||||
(single contiguous D2H copy, no Python per-body overhead).
|
(single contiguous D2H copy, no Python per-body overhead).
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
if self.bodies.obs_pinned is None:
|
if self.bodies.obs_pinned is None:
|
||||||
raise RuntimeError("No obs buffer. Call run() first.")
|
raise RuntimeError("No obs buffer. Call run() first.")
|
||||||
if stream is None:
|
if stream is None:
|
||||||
@@ -446,6 +461,7 @@ class Simulation:
|
|||||||
@context_bound
|
@context_bound
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initialize flow field and sync objects to GPU."""
|
"""Initialize flow field and sync objects to GPU."""
|
||||||
|
self._require_usable()
|
||||||
self._assert_allocation_contract()
|
self._assert_allocation_contract()
|
||||||
# Validate the complete geometry before flags, populations or modules
|
# Validate the complete geometry before flags, populations or modules
|
||||||
# change. Compact-list construction publishes no outputs on failure.
|
# change. Compact-list construction publishes no outputs on failure.
|
||||||
@@ -478,6 +494,7 @@ class Simulation:
|
|||||||
In EsoPull mode, the backing layout is decoded to physical DDF on
|
In EsoPull mode, the backing layout is decoded to physical DDF on
|
||||||
GPU for the patched cells only, patched on host, then encoded back.
|
GPU for the patched cells only, patched on host, then encoded back.
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
raise RuntimeError("Call initialize() before sync_bodies()")
|
raise RuntimeError("Call initialize() before sync_bodies()")
|
||||||
|
|
||||||
@@ -573,6 +590,7 @@ class Simulation:
|
|||||||
sensor) on GPU and reset the step accumulator before stepping.
|
sensor) on GPU and reset the step accumulator before stepping.
|
||||||
checkpoint_interval: Nonnegative integer; if >0, save every N steps.
|
checkpoint_interval: Nonnegative integer; if >0, save every N steps.
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
raise RuntimeError("Call initialize() first")
|
raise RuntimeError("Call initialize() first")
|
||||||
steps = operator.index(steps)
|
steps = operator.index(steps)
|
||||||
@@ -637,10 +655,12 @@ class Simulation:
|
|||||||
@context_bound
|
@context_bound
|
||||||
def get_macroscopic(self) -> Dict[str, np.ndarray]:
|
def get_macroscopic(self) -> Dict[str, np.ndarray]:
|
||||||
"""Download DDF and return rho, ux, uy [, uz]."""
|
"""Download DDF and return rho, ux, uy [, uz]."""
|
||||||
|
self._require_usable()
|
||||||
return self.field.get_macroscopic(step_id=self.stepper.step_count)
|
return self.field.get_macroscopic(step_id=self.stepper.step_count)
|
||||||
|
|
||||||
@context_bound
|
@context_bound
|
||||||
def get_ddf(self) -> np.ndarray:
|
def get_ddf(self) -> np.ndarray:
|
||||||
|
self._require_usable()
|
||||||
self.field.download_ddf(step_id=self.stepper.step_count)
|
self.field.download_ddf(step_id=self.stepper.step_count)
|
||||||
return self.field.ddf.copy()
|
return self.field.ddf.copy()
|
||||||
|
|
||||||
@@ -654,23 +674,27 @@ class Simulation:
|
|||||||
|
|
||||||
Accepted: omega, fx, fy, fz, n_objects.
|
Accepted: omega, fx, fy, fz, n_objects.
|
||||||
"""
|
"""
|
||||||
|
self._require_usable()
|
||||||
self.field.update_params(**kwargs)
|
self.field.update_params(**kwargs)
|
||||||
|
|
||||||
# -- Snapshots -----------------------------------------------------------
|
# -- Snapshots -----------------------------------------------------------
|
||||||
@context_bound
|
@context_bound
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
"""Snapshot the flow field; restore keeps the current clock and controls."""
|
"""Snapshot the flow field; restore keeps the current clock and controls."""
|
||||||
|
self._require_usable()
|
||||||
self.field.snapshot(step_id=self.stepper.step_count)
|
self.field.snapshot(step_id=self.stepper.step_count)
|
||||||
|
|
||||||
@context_bound
|
@context_bound
|
||||||
def restore(self):
|
def restore(self):
|
||||||
"""Restore the saved flow field at the current streaming parity."""
|
"""Restore the saved flow field at the current streaming parity."""
|
||||||
|
self._require_usable()
|
||||||
self.field.restore(step_id=self.stepper.step_count)
|
self.field.restore(step_id=self.stepper.step_count)
|
||||||
|
|
||||||
# -- Checkpoint / Restore (HDF5) ----------------------------------------
|
# -- Checkpoint / Restore (HDF5) ----------------------------------------
|
||||||
@context_bound
|
@context_bound
|
||||||
def save_checkpoint(self, path: str = None):
|
def save_checkpoint(self, path: str = None):
|
||||||
"""Save full simulation state to an HDF5 file."""
|
"""Save full simulation state to an HDF5 file."""
|
||||||
|
self._require_usable()
|
||||||
from .common.checkpoint import save_checkpoint
|
from .common.checkpoint import save_checkpoint
|
||||||
if self.bodies.jets:
|
if self.bodies.jets:
|
||||||
raise NotImplementedError("Jets are not checkpointed yet")
|
raise NotImplementedError("Jets are not checkpointed yet")
|
||||||
@@ -680,6 +704,7 @@ class Simulation:
|
|||||||
@context_bound
|
@context_bound
|
||||||
def load_checkpoint(self, path: str):
|
def load_checkpoint(self, path: str):
|
||||||
"""Restore simulation state from an HDF5 checkpoint."""
|
"""Restore simulation state from an HDF5 checkpoint."""
|
||||||
|
self._require_usable()
|
||||||
from .common.checkpoint import load_checkpoint
|
from .common.checkpoint import load_checkpoint
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Direct Simulation Jet control upload; boundary-law accuracy is tested elsewhere."""
|
||||||
|
import numpy as np
|
||||||
|
import pycuda.driver as cuda
|
||||||
|
|
||||||
|
from CelerisLab.body.jets import Jet, Radial, Sector
|
||||||
|
from CelerisLab.lbm.descriptors import FLUID
|
||||||
|
from ._cases import make_sim
|
||||||
|
|
||||||
|
|
||||||
|
def test_simulation_jet_amplitude_upload_and_continued_steps():
|
||||||
|
# Same grid/circle/sector as the existing uniform surface-actuation fixture.
|
||||||
|
center = (16., 12.)
|
||||||
|
with make_sim(nx=32, ny=24, velocity=0) as sim:
|
||||||
|
sim.add_body("circle", center=center, radius=4.3)
|
||||||
|
index = sim.add_jet(Jet(Sector(center, .7, .85, 4.4),
|
||||||
|
Radial(center, inward=False), "quartic", .002))
|
||||||
|
sim.initialize()
|
||||||
|
curved = sim.field.curved
|
||||||
|
shape, jet_id = curved.jet_shape.copy(), curved.jet_id.copy()
|
||||||
|
flags = sim.get_flags()
|
||||||
|
driven = jet_id == index
|
||||||
|
assert driven.any() and (~driven).any()
|
||||||
|
velocity_pointer = int(curved.surface_velocity_gpu)
|
||||||
|
previous = None
|
||||||
|
for interval, amplitude in enumerate((.002, -.001)):
|
||||||
|
if interval:
|
||||||
|
count = sim.stepper.step_count
|
||||||
|
sim.set_jet_amplitude(index, amplitude)
|
||||||
|
assert sim.stepper.step_count == count # update does not step
|
||||||
|
expected = np.zeros_like(curved.surface_velocity)
|
||||||
|
expected[driven] = (amplitude * shape[driven]).astype(np.float32)
|
||||||
|
actual = np.empty_like(expected)
|
||||||
|
with sim.ctx.activate():
|
||||||
|
cuda.memcpy_dtoh(actual, curved.surface_velocity_gpu)
|
||||||
|
np.testing.assert_array_equal(actual, expected) # actual device column
|
||||||
|
np.testing.assert_array_equal(curved.surface_velocity, expected)
|
||||||
|
assert np.any(actual[driven] != 0)
|
||||||
|
if previous is not None:
|
||||||
|
assert not np.array_equal(actual[driven], previous[driven])
|
||||||
|
previous = actual.copy()
|
||||||
|
assert sim.bodies.jets[index].amplitude == amplitude
|
||||||
|
assert curved.jets[index].amplitude == amplitude
|
||||||
|
assert int(curved.surface_velocity_gpu) == velocity_pointer
|
||||||
|
np.testing.assert_array_equal(curved.jet_shape, shape)
|
||||||
|
np.testing.assert_array_equal(curved.jet_id, jet_id)
|
||||||
|
np.testing.assert_array_equal(sim.get_flags(), flags)
|
||||||
|
sim.run(2)
|
||||||
|
assert sim.stepper.step_count == 2 * (interval + 1)
|
||||||
|
assert sim._failed is None
|
||||||
|
macro = sim.get_macroscopic()
|
||||||
|
fluid = (flags.reshape(24, 32) & FLUID) != 0
|
||||||
|
assert all(np.isfinite(value[fluid]).all() for value in macro.values())
|
||||||
|
assert np.all(macro["rho"][fluid] > 0)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Jet amplitude update failure contract (host doubles only, no CUDA numerics).
|
||||||
|
|
||||||
|
Both owners validate before any device write, commit host state only after the
|
||||||
|
upload returns, and refuse every later call except close() after an uncertain
|
||||||
|
device write.
|
||||||
|
"""
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("pycuda.driver")
|
||||||
|
from CelerisLab.body.jets import Jet, Radial, Sector
|
||||||
|
from tests.unit.test_pinned_body_remap import ready
|
||||||
|
from tests.unit.test_simulation_lifecycle import make_simulation
|
||||||
|
|
||||||
|
BAD = (float("nan"), float("inf"), 1.)
|
||||||
|
|
||||||
|
|
||||||
|
class Curved:
|
||||||
|
"""Records uploads; ``fail`` raises once the write was attempted."""
|
||||||
|
def __init__(self):
|
||||||
|
self.writes, self.fail = [], False
|
||||||
|
|
||||||
|
def set_jet_amplitude(self, index, amplitude, stream):
|
||||||
|
self.writes.append((index, amplitude))
|
||||||
|
if self.fail:
|
||||||
|
raise RuntimeError("upload failed")
|
||||||
|
|
||||||
|
|
||||||
|
def simulation(monkeypatch):
|
||||||
|
sim = make_simulation(monkeypatch)
|
||||||
|
jet = Jet(Sector((6., 5.), 0., .5, 4.), Radial((6., 5.)), "uniform", 1e-3)
|
||||||
|
sim._failed, sim._initialized = None, True
|
||||||
|
sim.bodies, sim.field = SimpleNamespace(jets=[jet]), SimpleNamespace(curved=Curved())
|
||||||
|
return sim, jet
|
||||||
|
|
||||||
|
|
||||||
|
def test_simulation_invalid_input_writes_nothing_and_stays_usable(monkeypatch):
|
||||||
|
sim, jet = simulation(monkeypatch)
|
||||||
|
for amplitude in BAD:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
sim.set_jet_amplitude(0, amplitude)
|
||||||
|
with pytest.raises(IndexError):
|
||||||
|
sim.set_jet_amplitude(1, 2e-3)
|
||||||
|
assert sim.field.curved.writes == [] and sim.bodies.jets[0] is jet and sim._failed is None
|
||||||
|
sim.set_jet_amplitude(0, 2e-3) # retry after a rejected input
|
||||||
|
assert sim.field.curved.writes == [(0, 2e-3)] and sim.bodies.jets[0].amplitude == 2e-3
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_simulation_upload_failure_keeps_host_and_blocks_reuse(monkeypatch):
|
||||||
|
sim, jet = simulation(monkeypatch)
|
||||||
|
sim.field.curved.fail = True
|
||||||
|
with pytest.raises(RuntimeError, match="upload failed"):
|
||||||
|
sim.set_jet_amplitude(0, 2e-3)
|
||||||
|
assert sim.bodies.jets[0] is jet and sim._failed is not None
|
||||||
|
sim.field.curved.fail = False
|
||||||
|
for call in (lambda: sim.set_jet_amplitude(0, 2e-3), lambda: sim.run(1), sim.step,
|
||||||
|
sim.sync_bodies, sim.initialize, sim.get_ddf, sim.get_macroscopic,
|
||||||
|
lambda: sim.read_surface_exchange(0), lambda: sim.read_body(0),
|
||||||
|
sim.read_bodies, sim.snapshot, sim.restore, sim.save_checkpoint,
|
||||||
|
lambda: sim.load_checkpoint("x.h5"), lambda: sim.update_runtime_params(omega=1.)):
|
||||||
|
with pytest.raises(RuntimeError, match="Simulation failed"):
|
||||||
|
call()
|
||||||
|
assert sim.field.curved.writes == [(0, 2e-3)]
|
||||||
|
sim.close() # cleanup still works
|
||||||
|
assert sim._closed and sim.ctx.is_closed
|
||||||
|
|
||||||
|
|
||||||
|
def test_simulation_before_initialize_commits_host_only(monkeypatch):
|
||||||
|
sim, _ = simulation(monkeypatch)
|
||||||
|
sim._initialized = False
|
||||||
|
sim.set_jet_amplitude(0, -2e-3)
|
||||||
|
assert sim.field.curved.writes == [] and sim.bodies.jets[0].amplitude == -2e-3
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
|
||||||
|
def stepper_state(run):
|
||||||
|
boundary = run._level_boundaries[2]
|
||||||
|
return boundary.tables["surface_velocity"].copy(), boundary.jets, run._level_jets[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_stepper_invalid_input_writes_nothing_and_stays_ready():
|
||||||
|
run, module, _, _, _ = ready()
|
||||||
|
try:
|
||||||
|
velocity, jets, level_jets = stepper_state(run)
|
||||||
|
module.cuda.memcpy_htod.reset_mock()
|
||||||
|
for args, kw, error in [((0, a), {"level": 2}, ValueError) for a in BAD] + [
|
||||||
|
((1, .01), {"level": 2}, IndexError), ((0, .01), {"level": 3}, ValueError)]:
|
||||||
|
with pytest.raises(error):
|
||||||
|
run.set_jet_amplitude(*args, **kw)
|
||||||
|
module.cuda.memcpy_htod.assert_not_called()
|
||||||
|
np.testing.assert_array_equal(run._level_boundaries[2].tables["surface_velocity"], velocity)
|
||||||
|
assert (run._level_boundaries[2].jets, run._level_jets[2]) == (jets, level_jets)
|
||||||
|
assert run._status == "ready"
|
||||||
|
run.set_jet_amplitude(0, .017, level=2)
|
||||||
|
assert run._level_jets[2][0].amplitude == .017
|
||||||
|
finally:
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("where", ["upload", "synchronize"])
|
||||||
|
def test_stepper_device_failure_keeps_host_and_blocks_reuse(where):
|
||||||
|
run, module, _, _, _ = ready()
|
||||||
|
try:
|
||||||
|
velocity, jets, level_jets = stepper_state(run)
|
||||||
|
if where == "upload":
|
||||||
|
module.cuda.memcpy_htod.side_effect = RuntimeError("upload failed")
|
||||||
|
else:
|
||||||
|
run._stream.synchronize.side_effect = RuntimeError("upload failed")
|
||||||
|
with pytest.raises(RuntimeError, match="upload failed"):
|
||||||
|
run.set_jet_amplitude(0, .017, level=2)
|
||||||
|
np.testing.assert_array_equal(run._level_boundaries[2].tables["surface_velocity"], velocity)
|
||||||
|
assert (run._level_boundaries[2].jets, run._level_jets[2]) == (jets, level_jets)
|
||||||
|
assert run._status == "failed"
|
||||||
|
module.cuda.memcpy_htod.side_effect = None
|
||||||
|
run._stream.synchronize.side_effect = None
|
||||||
|
for call in (lambda: run.set_jet_amplitude(0, .017, level=2), run.step, run.download):
|
||||||
|
with pytest.raises(RuntimeError, match="failed"):
|
||||||
|
call()
|
||||||
|
finally:
|
||||||
|
run.close()
|
||||||
|
assert run._status == "closed"
|
||||||
@@ -194,6 +194,7 @@ def test_public_surface_exchange_for_jet_preserves_aperture_at_zero_and_reverse(
|
|||||||
field, bodies = jet_case(device, [jet])
|
field, bodies = jet_case(device, [jet])
|
||||||
sim = SimpleNamespace(bodies=bodies, field=field, _initialized=True,
|
sim = SimpleNamespace(bodies=bodies, field=field, _initialized=True,
|
||||||
stream=SimpleNamespace(synchronize=device.synchronize))
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
sim._require_usable = Simulation._require_usable.__get__(sim)
|
||||||
cl = field.curved
|
cl = field.curved
|
||||||
assert bodies.get(0).surface_actuation is None
|
assert bodies.get(0).surface_actuation is None
|
||||||
aperture, velocity = cl.aperture, cl.surface_velocity.copy()
|
aperture, velocity = cl.aperture, cl.surface_velocity.copy()
|
||||||
@@ -218,6 +219,7 @@ def test_public_surface_exchange_keeps_legacy_and_passive_guards(device):
|
|||||||
bodies.sync_to_gpu(field)
|
bodies.sync_to_gpu(field)
|
||||||
sim = SimpleNamespace(bodies=bodies, field=field,
|
sim = SimpleNamespace(bodies=bodies, field=field,
|
||||||
stream=SimpleNamespace(synchronize=device.synchronize))
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
sim._require_usable = Simulation._require_usable.__get__(sim)
|
||||||
for value in Simulation.read_surface_exchange(sim, 0).values():
|
for value in Simulation.read_surface_exchange(sim, 0).values():
|
||||||
np.testing.assert_array_equal(value, 0.)
|
np.testing.assert_array_equal(value, 0.)
|
||||||
field.curved.free_gpu_columns()
|
field.curved.free_gpu_columns()
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ def test_passive_overlap_failure_keeps_live_state(device, entry, second_x):
|
|||||||
_initialized=entry == "runtime_edit",
|
_initialized=entry == "runtime_edit",
|
||||||
_assert_allocation_contract=lambda: None,
|
_assert_allocation_contract=lambda: None,
|
||||||
stream=SimpleNamespace(synchronize=device.synchronize))
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
sim._require_usable = Simulation._require_usable.__get__(sim)
|
||||||
with pytest.raises(ValueError, match="active fluid receivers|unique cut-link writers"):
|
with pytest.raises(ValueError, match="active fluid receivers|unique cut-link writers"):
|
||||||
if entry == "sync":
|
if entry == "sync":
|
||||||
bodies.sync_to_gpu(field)
|
bodies.sync_to_gpu(field)
|
||||||
|
|||||||
Reference in New Issue
Block a user