"""Research interface contracts; no CUDA context or physical validation.""" import importlib.util from pathlib import Path from types import SimpleNamespace from unittest.mock import patch import numpy as np import pytest ROOT = Path(__file__).resolve().parents[2] spec = importlib.util.spec_from_file_location('karman_viv_research_driver', ROOT/'research/karman_viv/run.py') m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) def test_observation_unit_mapping_and_order(): norm = dict(force_norm_fact=2.,sens_deviation=np.arange(6)/10.,sens_norm_fact=np.ones(6)*3) s=np.arange(6)*.001; f=np.arange(6)*.01 raw,obs,nclip=m.observation(s,f,2,norm) np.testing.assert_array_equal(raw,np.asarray([*(s*156),*(f*2)],np.float32)) expected=np.hstack((raw[6:]/2,(raw[:6]-np.asarray(norm['sens_deviation'],np.float32))/3)) np.testing.assert_array_equal(obs,expected.astype(np.float32)) assert nclip==0 def test_same_physical_signal_d20_d40(): norm=dict(force_norm_fact=1.,sens_deviation=np.zeros(6),sens_norm_fact=np.ones(6)) a=m.observation(np.ones(6)*.001,np.arange(6)*.01,1,norm) b=m.observation(np.ones(6)*.0005,np.arange(6)*.005,2,norm) np.testing.assert_array_equal(a[0],b[0]); np.testing.assert_array_equal(a[1],b[1]) def test_zero_action_and_ccw_scale(): np.testing.assert_array_equal(m.wall_target(-m.BIAS/8,2),np.zeros(3)) np.testing.assert_allclose(m.wall_target(np.ones(3),2),[.04,.02,.06]) assert m.wall_target(np.ones(3),2)[0]/20 == .002 def test_ema_equal_physical_time_and_interval_reset(): target=np.array([.01,-.02,.03]); coarse=np.zeros(3); fine=np.zeros(3) for _ in range(13): coarse=m.ema(coarse,target,1) for _ in range(52): fine=m.ema(fine,target,2) np.testing.assert_allclose(fine,coarse,rtol=2e-15,atol=1e-17) np.testing.assert_allclose(m.ema(np.zeros(3),target,2),(1-.9**.25)*target) def test_config_real_parser(tmp_path): import json from CelerisLab.config import load_lbm_config path=tmp_path/'config.json';path.write_text(json.dumps(m.config(40))) cfg=load_lbm_config(str(path));cfg.validate() assert (cfg.nx,cfg.ny,cfg.velocity,cfg.viscosity)==(2559,1022,.005,.004) def test_structure_physical_scaling_and_no_damping(): coarse=m.Structure(2,1e-4,0,20);fine=m.Structure(2,2.5e-5,0,40) assert fine.mass==4*coarse.mass assert fine.stiffness==coarse.stiffness/4 assert fine.damping==0 assert fine.candidate(0)==(0.,0.) def test_observation_clipping_and_nonfinite(): norm=dict(force_norm_fact=1.,sens_deviation=np.zeros(6),sens_norm_fact=np.ones(6)) _,obs,n=m.observation(np.ones(6),np.ones(6),2,norm) assert n==12 and np.array_equal(obs,np.ones(12,np.float32)) with pytest.raises(RuntimeError,match='nonfinite'): m.observation(np.full(6,np.nan),np.zeros(6),2,norm) def test_arm_step_order_and_native_rhs(tmp_path): import CelerisLab from CelerisLab.body.motion import MovingTables from contextlib import nullcontext log=[];candidates=[] class Structure: y=v=0.;mass=2.;stiffness=3.;frequency=.1 def __init__(self,*args): pass def candidate(self,f): candidates.append(f) return self.y+.001,self.v+.002 class Sim: def __init__(self,*args): self.n=0 def __enter__(self): return self def __exit__(self,*args): pass def initialize(self): pass def set_body(self,p,**kw): log.append(('set',p,kw)) def run(self,n,**kw): self.n+=1;log.append(('run',n,kw)) def moving_diagnostics(self): a=np.zeros((3,len(MovingTables.DIAG)),np.float32) a[:,MovingTables.DIAG.index('cme_y')]=self.n a[:,MovingTables.DIAG.index('wen_y')]=10*self.n a[:,MovingTables.DIAG.index('cover_jy')]=1000 return a def read_force(self,p): return [0.,float(self.n),0.] def read_sensor(self,p): return [.001,0.,0.] seen=[] model=SimpleNamespace(predict=lambda obs,**kw:(seen.append(obs.copy()) or np.ones(3)*.1,None)) norm=dict(force_norm_fact=1.,sens_deviation=np.zeros(6),sens_norm_fact=np.ones(6)) with patch.object(CelerisLab,'Simulation',Sim),patch.object(m,'Structure',Structure), \ patch.object(m,'add_scene',return_value=([1,2,3],[4,5,6])),patch.object(m,'install_seed',return_value={}), \ patch.object(m,'check_time'): m.run_arm('controlled','cfg',2,2,2,None,7.,model,norm,tmp_path,0,1) assert candidates==[7.,33.,66.,99.] # previous native force, NOT SI mean or node impulse assert not seen[0].any() and len(seen)==2 assert [e[0] for e in log]==['set','set','set','run']*4 for e in log: if e[0]=='set': assert e[2]['vy']==pytest.approx(.001) and e[2]['omega']!=0 else: assert e[2]==dict(zero_obs=True,sync_obs=True) rows=np.load(tmp_path/'controlled/steps.npy') np.testing.assert_allclose(rows[:,1],[.001,.002,.003,.004]) assert rows.shape==(4,18) np.testing.assert_array_equal(rows[0,15:],rows[2,15:]) # EMA resets at SI boundary signals=np.load(tmp_path/'controlled/signals.npz') np.testing.assert_array_equal(signals['forces'][:,1],[3.,7.]) # interval CME *2, not CME+Wen