Wire Jets through ObjectManager and Simulation
ObjectManager.jets holds scene-level Jets. build_compact_lists_for collects true FP64 hits (SimObject.get_curved_list hits_out) row-aligned with the cut-link columns; BodySyncPlan carries them as curved_hits, and sync_to_gpu / apply_sync_plan / the pre-sync check pass jets+hits to assign_surface. Jets require D2Q9/FP32/unshifted/double_buffer and may only drive links of stationary bodies. Simulation.add_jet (before initialize) and set_jet_amplitude (velocity column re-upload only); save_checkpoint refuses scenes with jets. Fine-path wiring not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
611a64c54a
commit
932702f779
6 files changed
+112
-11
No files matched your search
@@ -70,6 +70,7 @@ class ObjectManager:
|
|||||||
self.nq = nq
|
self.nq = nq
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self._registry = BodyRegistry()
|
self._registry = BodyRegistry()
|
||||||
|
self.jets: list = [] # scene-level Jets; selection is spatial, not per body
|
||||||
|
|
||||||
self.action_gpu: Optional[cuda.DeviceAllocation] = None
|
self.action_gpu: Optional[cuda.DeviceAllocation] = None
|
||||||
self.obs_gpu: Optional[cuda.DeviceAllocation] = None
|
self.obs_gpu: Optional[cuda.DeviceAllocation] = None
|
||||||
@@ -247,6 +248,7 @@ class ObjectManager:
|
|||||||
|
|
||||||
# Build compact lists for new object set
|
# Build compact lists for new object set
|
||||||
new_sensor_counts = np.zeros(max(next_count, 1), dtype=np.int32)
|
new_sensor_counts = np.zeros(max(next_count, 1), dtype=np.int32)
|
||||||
|
hits = []
|
||||||
(
|
(
|
||||||
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
||||||
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
||||||
@@ -254,7 +256,7 @@ class ObjectManager:
|
|||||||
fr_cells, fr_obj_id,
|
fr_cells, fr_obj_id,
|
||||||
) = self.build_compact_lists_for(
|
) = self.build_compact_lists_for(
|
||||||
next_objects, domain_flags=next_flags,
|
next_objects, domain_flags=next_flags,
|
||||||
sensor_cell_counts_out=new_sensor_counts,
|
sensor_cell_counts_out=new_sensor_counts, hits_out=hits,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compute change masks
|
# Compute change masks
|
||||||
@@ -277,6 +279,7 @@ class ObjectManager:
|
|||||||
added_solid_mask=added_solid_mask,
|
added_solid_mask=added_solid_mask,
|
||||||
released_fluid_mask=released_fluid_mask,
|
released_fluid_mask=released_fluid_mask,
|
||||||
new_sensor_counts=new_sensor_counts,
|
new_sensor_counts=new_sensor_counts,
|
||||||
|
curved_hits=hits[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
def commit_pending(self, next_objects: List[SimObject],
|
def commit_pending(self, next_objects: List[SimObject],
|
||||||
@@ -309,7 +312,8 @@ class ObjectManager:
|
|||||||
|
|
||||||
cl_host = plan.curved_host
|
cl_host = plan.curved_host
|
||||||
field.curved.assign_host(*cl_host)
|
field.curved.assign_host(*cl_host)
|
||||||
field.curved.assign_surface(plan.next_objects, self.cfg, plan.next_flags)
|
field.curved.assign_surface(plan.next_objects, self.cfg, plan.next_flags,
|
||||||
|
jets=self.jets, hits=plan.curved_hits)
|
||||||
s_cells, s_ids = plan.sensor_host
|
s_cells, s_ids = plan.sensor_host
|
||||||
field.sensors.assign_host(s_cells, s_ids)
|
field.sensors.assign_host(s_cells, s_ids)
|
||||||
fr_cells, fr_ids = plan.force_region_host
|
fr_cells, fr_ids = plan.force_region_host
|
||||||
@@ -344,13 +348,13 @@ class ObjectManager:
|
|||||||
return merge_flag_masks(base_flags, masks)
|
return merge_flag_masks(base_flags, masks)
|
||||||
|
|
||||||
# -- Compact list building -----------------------------------------------
|
# -- Compact list building -----------------------------------------------
|
||||||
def build_compact_lists(self, domain_flags: np.ndarray | None = None):
|
def build_compact_lists(self, domain_flags: np.ndarray | None = None, *, hits_out=None):
|
||||||
"""Build cut-link SoA columns, sensor lists, and force-region lists
|
"""Build cut-link SoA columns, sensor lists, and force-region lists
|
||||||
from the formal registry objects.
|
from the formal registry objects.
|
||||||
"""
|
"""
|
||||||
return self.build_compact_lists_for(
|
return self.build_compact_lists_for(
|
||||||
self._registry.objects, domain_flags,
|
self._registry.objects, domain_flags,
|
||||||
sensor_cell_counts_out=self.sensor_cell_counts,
|
sensor_cell_counts_out=self.sensor_cell_counts, hits_out=hits_out,
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_compact_lists_for(
|
def build_compact_lists_for(
|
||||||
@@ -359,6 +363,7 @@ class ObjectManager:
|
|||||||
domain_flags: np.ndarray | None = None,
|
domain_flags: np.ndarray | None = None,
|
||||||
*,
|
*,
|
||||||
sensor_cell_counts_out: np.ndarray | None = None,
|
sensor_cell_counts_out: np.ndarray | None = None,
|
||||||
|
hits_out: list | None = None,
|
||||||
):
|
):
|
||||||
"""Build cut-link SoA columns, sensor lists, and force-region lists.
|
"""Build cut-link SoA columns, sensor lists, and force-region lists.
|
||||||
|
|
||||||
@@ -367,6 +372,8 @@ class ObjectManager:
|
|||||||
domain_flags: Optional uint16 flags for donor-cell classification.
|
domain_flags: Optional uint16 flags for donor-cell classification.
|
||||||
sensor_cell_counts_out: Int array to fill with per-object sensor
|
sensor_cell_counts_out: Int array to fill with per-object sensor
|
||||||
cell counts. If provided, ``fill(0)`` is called first.
|
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).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
cl_fluid_idx, cl_dir, cl_q, cl_body_id, cl_rx, cl_ry, cl_rz,
|
cl_fluid_idx, cl_dir, cl_q, cl_body_id, cl_rx, cl_ry, cl_rz,
|
||||||
@@ -376,6 +383,7 @@ class ObjectManager:
|
|||||||
"""
|
"""
|
||||||
cl_idx, cl_dir, cl_q, cl_bid = [], [], [], []
|
cl_idx, cl_dir, cl_q, cl_bid = [], [], [], []
|
||||||
cl_rx, cl_ry, cl_rz, cl_fallback = [], [], [], []
|
cl_rx, cl_ry, cl_rz, cl_fallback = [], [], [], []
|
||||||
|
cl_hit = []
|
||||||
s_cells, s_ids = [], []
|
s_cells, s_ids = [], []
|
||||||
fr_cells, fr_ids = [], []
|
fr_cells, fr_ids = [], []
|
||||||
if domain_flags is None:
|
if domain_flags is None:
|
||||||
@@ -404,7 +412,7 @@ class ObjectManager:
|
|||||||
) = obj.get_curved_list(
|
) = obj.get_curved_list(
|
||||||
self.nx, self.ny, self.nq,
|
self.nx, self.ny, self.nq,
|
||||||
nz=self.nz, ex=ex, ey=ey, ez=ez,
|
nz=self.nz, ex=ex, ey=ey, ez=ez,
|
||||||
domain_flags=domain_flags,
|
domain_flags=domain_flags, hits_out=cl_hit,
|
||||||
)
|
)
|
||||||
if len(fluid_idx) > 0:
|
if len(fluid_idx) > 0:
|
||||||
cl_idx.append(fluid_idx)
|
cl_idx.append(fluid_idx)
|
||||||
@@ -452,12 +460,15 @@ class ObjectManager:
|
|||||||
r_fr_cells = np.concatenate(fr_cells) if fr_cells else np.zeros(0, dtype=np.uint32)
|
r_fr_cells = np.concatenate(fr_cells) if fr_cells else np.zeros(0, dtype=np.uint32)
|
||||||
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
||||||
|
|
||||||
if any(obj.surface_actuation is not None for obj in objects):
|
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
|
||||||
|
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.
|
# Reject unresolved apertures/ownership before sync or restart mutates state.
|
||||||
from ..lbm.curved_links import CurvedLinkSoA
|
from ..lbm.curved_links import CurvedLinkSoA
|
||||||
links = CurvedLinkSoA()
|
links = CurvedLinkSoA()
|
||||||
links.assign_host(r_idx, r_dir, r_q, r_bid, r_rx, r_ry, r_rz, r_fallback)
|
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)
|
links.assign_surface(objects, self.cfg, domain_flags, jets=self.jets, hits=r_hit)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
r_idx, r_dir, r_q, r_bid,
|
r_idx, r_dir, r_q, r_bid,
|
||||||
@@ -479,13 +490,14 @@ class ObjectManager:
|
|||||||
self._telemetry_field = field
|
self._telemetry_field = field
|
||||||
|
|
||||||
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
||||||
|
hits = []
|
||||||
|
|
||||||
(
|
(
|
||||||
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
||||||
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
||||||
sensor_cells, sensor_obj_id,
|
sensor_cells, sensor_obj_id,
|
||||||
fr_cells, fr_obj_id,
|
fr_cells, fr_obj_id,
|
||||||
) = self.build_compact_lists(domain_flags=next_flags)
|
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
||||||
|
|
||||||
if rebuild_flags:
|
if rebuild_flags:
|
||||||
field.flag = next_flags
|
field.flag = next_flags
|
||||||
@@ -495,7 +507,8 @@ class ObjectManager:
|
|||||||
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
||||||
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
||||||
)
|
)
|
||||||
field.curved.assign_surface(self.objects, self.cfg, field.flag)
|
field.curved.assign_surface(self.objects, self.cfg, field.flag,
|
||||||
|
jets=self.jets, hits=hits[0])
|
||||||
field.sensors.assign_host(sensor_cells, sensor_obj_id)
|
field.sensors.assign_host(sensor_cells, sensor_obj_id)
|
||||||
field.force_regions.assign_host(fr_cells, fr_obj_id)
|
field.force_regions.assign_host(fr_cells, fr_obj_id)
|
||||||
|
|
||||||
|
|||||||
@@ -195,12 +195,13 @@ class SimObject:
|
|||||||
def get_curved_list(
|
def get_curved_list(
|
||||||
self, nx: int, ny: int, nq: int,
|
self, nx: int, ny: int, nq: int,
|
||||||
nz: int = 1, ex=None, ey=None, ez=None,
|
nz: int = 1, ex=None, ey=None, ez=None,
|
||||||
domain_flags=None,
|
domain_flags=None, hits_out=None,
|
||||||
):
|
):
|
||||||
"""Return 8-column SoA arrays for curved-boundary kernel.
|
"""Return 8-column SoA arrays for curved-boundary kernel.
|
||||||
|
|
||||||
Delegates to the geometry object for cut-link enumeration and
|
Delegates to the geometry object for cut-link enumeration and
|
||||||
to ``pack_cut_links_to_soa`` for SoA packing.
|
to ``pack_cut_links_to_soa`` for SoA packing. If ``hits_out`` is a
|
||||||
|
list, the true FP64 hit points (n, 2) are appended row-aligned.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
8-tuple matching CurvedLinkSoA.assign_host().
|
8-tuple matching CurvedLinkSoA.assign_host().
|
||||||
@@ -210,6 +211,9 @@ class SimObject:
|
|||||||
links = self.geometry.build_cut_links(nx, ny, domain_flags=domain_flags)
|
links = self.geometry.build_cut_links(nx, ny, domain_flags=domain_flags)
|
||||||
for cl in links:
|
for cl in links:
|
||||||
cl.body_id = self.obj_id
|
cl.body_id = self.obj_id
|
||||||
|
if hits_out is not None:
|
||||||
|
hits_out.append(np.array([(cl.hit_x, cl.hit_y) for cl in links],
|
||||||
|
np.float64).reshape(-1, 2))
|
||||||
return pack_cut_links_to_soa(links)
|
return pack_cut_links_to_soa(links)
|
||||||
|
|
||||||
def get_sensor_list(self, nx: int, ny: int, nz: int = 1):
|
def get_sensor_list(self, nx: int, ny: int, nz: int = 1):
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class BodySyncPlan:
|
|||||||
added_solid_mask: Bool array -- True where ``old_fluid -> new_solid``.
|
added_solid_mask: Bool array -- True where ``old_fluid -> new_solid``.
|
||||||
released_fluid_mask: Bool array -- True where ``old_obstacle -> new_fluid``.
|
released_fluid_mask: Bool array -- True where ``old_obstacle -> new_fluid``.
|
||||||
new_sensor_counts: Int array of sensor cell counts per object.
|
new_sensor_counts: Int array of sensor cell counts per object.
|
||||||
|
curved_hits: FP64 (n, 2) true hit points row-aligned with curved_host.
|
||||||
"""
|
"""
|
||||||
next_objects: List[SimObject]
|
next_objects: List[SimObject]
|
||||||
next_count: int
|
next_count: int
|
||||||
@@ -50,3 +51,4 @@ class BodySyncPlan:
|
|||||||
added_solid_mask: np.ndarray
|
added_solid_mask: np.ndarray
|
||||||
released_fluid_mask: np.ndarray
|
released_fluid_mask: np.ndarray
|
||||||
new_sensor_counts: np.ndarray
|
new_sensor_counts: np.ndarray
|
||||||
|
curved_hits: np.ndarray | None = None
|
||||||
@@ -73,8 +73,16 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
|||||||
from ..body.jets import jet_link_shapes, jet_link_velocity
|
from ..body.jets import jet_link_shapes, jet_link_velocity
|
||||||
if hits is None or len(hits) != len(dir):
|
if hits is None or len(hits) != len(dir):
|
||||||
raise ValueError("Jets require true hit points row-aligned with the cut links")
|
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):
|
||||||
|
raise ValueError("Jets require D2Q9/FP32/unshifted/double_buffer")
|
||||||
shape, jet_id = jet_link_shapes(jets, hits)
|
shape, jet_id = jet_link_shapes(jets, hits)
|
||||||
driven = jet_id >= 0
|
driven = jet_id >= 0
|
||||||
|
owners = {obj.obj_id: obj for obj in objects}
|
||||||
|
for bid in np.unique(body_id[driven]):
|
||||||
|
state = vars(owners[int(bid)].state)
|
||||||
|
if any(state.get(k, 0.) != 0 for k in ("vx", "vy", "omega")):
|
||||||
|
raise ValueError("Jets may only drive links of stationary bodies")
|
||||||
if np.any(surface_aperture & driven):
|
if np.any(surface_aperture & driven):
|
||||||
raise ValueError("Jet overlaps a SurfaceActuation aperture")
|
raise ValueError("Jet overlaps a SurfaceActuation aperture")
|
||||||
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
||||||
|
|||||||
@@ -157,6 +157,31 @@ class Simulation:
|
|||||||
return -1
|
return -1
|
||||||
return self.bodies.add(obj)
|
return self.bodies.add(obj)
|
||||||
|
|
||||||
|
def add_jet(self, jet) -> int:
|
||||||
|
"""Register a body-independent Jet before ``initialize()``; returns its index.
|
||||||
|
|
||||||
|
The jet drives every scene cut link whose true hit point lies in its
|
||||||
|
region (overlaps rejected at initialize). Not checkpointed.
|
||||||
|
"""
|
||||||
|
from .body.jets import Jet
|
||||||
|
if not isinstance(jet, Jet):
|
||||||
|
raise TypeError("add_jet expects a Jet")
|
||||||
|
if self._initialized:
|
||||||
|
raise RuntimeError("Jets must be added before initialize()")
|
||||||
|
self.bodies.jets.append(jet)
|
||||||
|
return len(self.bodies.jets) - 1
|
||||||
|
|
||||||
|
@context_bound
|
||||||
|
def set_jet_amplitude(self, index: int, amplitude: float) -> None:
|
||||||
|
"""Change one jet's signed amplitude; re-uploads per-link velocity only."""
|
||||||
|
if self._initialized:
|
||||||
|
self.field.curved.set_jet_amplitude(index, amplitude, self.stream)
|
||||||
|
return
|
||||||
|
from .body.jets import Jet
|
||||||
|
jet = self.bodies.jets[index]
|
||||||
|
Jet(jet.region, jet.field, jet.profile, amplitude) # validate before mutation
|
||||||
|
jet.amplitude = float(amplitude)
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -433,6 +458,8 @@ class Simulation:
|
|||||||
self.field.flag = self.bodies.build_flags(self.field.flag)
|
self.field.flag = self.bodies.build_flags(self.field.flag)
|
||||||
self.field.upload_flags()
|
self.field.upload_flags()
|
||||||
|
|
||||||
|
if self.bodies.jets and self.bodies.count == 0:
|
||||||
|
raise ValueError("Jets need bodies with cut links")
|
||||||
self.stepper.initialize()
|
self.stepper.initialize()
|
||||||
if self.bodies.count > 0:
|
if self.bodies.count > 0:
|
||||||
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
||||||
@@ -644,6 +671,8 @@ class Simulation:
|
|||||||
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."""
|
||||||
from .common.checkpoint import save_checkpoint
|
from .common.checkpoint import save_checkpoint
|
||||||
|
if self.bodies.jets:
|
||||||
|
raise NotImplementedError("Jets are not checkpointed yet")
|
||||||
return save_checkpoint(self.field, self.stepper, self.lbm_cfg,
|
return save_checkpoint(self.field, self.stepper, self.lbm_cfg,
|
||||||
self.bodies, path)
|
self.bodies, path)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Body-independent Jet: true-hit spatial selection, fields, profiles, overlap, amplitude."""
|
"""Body-independent Jet: true-hit spatial selection, fields, profiles, overlap, amplitude."""
|
||||||
import math
|
import math
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
@@ -8,6 +9,7 @@ from CelerisLab.body.jets import (Jet, Parallel, Radial, Sector, Tube,
|
|||||||
fine_hits_to_physical, jet_link_shapes)
|
fine_hits_to_physical, jet_link_shapes)
|
||||||
from CelerisLab.body.objects import SurfaceActuation
|
from CelerisLab.body.objects import SurfaceActuation
|
||||||
from CelerisLab.lbm.curved_links import CurvedLinkSoA, surface_link_velocity
|
from CelerisLab.lbm.curved_links import CurvedLinkSoA, surface_link_velocity
|
||||||
|
from .test_state_regressions import device, make_field, manager
|
||||||
from .test_surface_actuation import circle, projected
|
from .test_surface_actuation import circle, projected
|
||||||
|
|
||||||
CENTER, RADIUS = (6., 5.), 2.999
|
CENTER, RADIUS = (6., 5.), 2.999
|
||||||
@@ -132,3 +134,46 @@ def test_legacy_adapter_measured_equivalence(spec):
|
|||||||
diff = np.abs(cl.surface_velocity - legacy.surface_velocity).max()
|
diff = np.abs(cl.surface_velocity - legacy.surface_velocity).max()
|
||||||
print(f"legacy adapter max |dU| = {diff:.3e} (exact={diff == 0})")
|
print(f"legacy adapter max |dU| = {diff:.3e} (exact={diff == 0})")
|
||||||
assert diff <= 4*np.finfo(np.float32).eps*abs(spec.normal_velocity)
|
assert diff <= 4*np.finfo(np.float32).eps*abs(spec.normal_velocity)
|
||||||
|
|
||||||
|
|
||||||
|
def jet_case(device, jets):
|
||||||
|
field = make_field(device)
|
||||||
|
bodies = manager(field, [circle(radius=2.)])
|
||||||
|
bodies.jets.extend(jets)
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
return field, bodies
|
||||||
|
|
||||||
|
|
||||||
|
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
|
||||||
|
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
|
||||||
|
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||||
|
field, bodies = jet_case(device, [jet])
|
||||||
|
cl = field.curved
|
||||||
|
assert cl.jet_id is not None and (cl.jet_id == 0).sum() >= 2
|
||||||
|
velocity_gpu = cl.surface_velocity_gpu
|
||||||
|
np.testing.assert_array_equal(np.frombuffer(velocity_gpu.data, np.float32).reshape(-1, 2),
|
||||||
|
cl.surface_velocity)
|
||||||
|
geometry = {name: bytes(getattr(cl, name + "_gpu").data)
|
||||||
|
for name in ("fluid_idx", "dir", "q", "body_id", "rx", "ry")}
|
||||||
|
writes, before = device.writes, cl.surface_velocity.copy()
|
||||||
|
cl.set_jet_amplitude(0, -1e-3, SimpleNamespace(synchronize=lambda: None))
|
||||||
|
assert device.writes == writes + 1 and cl.surface_velocity_gpu is velocity_gpu
|
||||||
|
np.testing.assert_array_equal(np.frombuffer(velocity_gpu.data, np.float32).reshape(-1, 2),
|
||||||
|
cl.surface_velocity)
|
||||||
|
assert all(bytes(getattr(cl, name + "_gpu").data) == data for name, data in geometry.items())
|
||||||
|
np.testing.assert_allclose(cl.surface_velocity, -.5*before, rtol=1e-6, atol=0)
|
||||||
|
np.testing.assert_array_equal(cl.surface_velocity[cl.jet_id < 0], 0.)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manager_rejects_overlapping_jets_and_moving_bodies(device):
|
||||||
|
a = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER))
|
||||||
|
b = Jet(Tube(CENTER, 0., 3., .5), Parallel(0.))
|
||||||
|
with pytest.raises(ValueError, match="overlaps"):
|
||||||
|
jet_case(device, [a, b])
|
||||||
|
field = make_field(device)
|
||||||
|
moving = circle(radius=2.)
|
||||||
|
moving.state.omega = 1e-3
|
||||||
|
bodies = manager(field, [moving])
|
||||||
|
bodies.jets.append(a)
|
||||||
|
with pytest.raises(ValueError, match="stationary"):
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
Reference in new issue
Block a user