Add one predeclared mass-four paired VIV exploration
This commit is contained in:
@@ -81,3 +81,40 @@ CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src python research/karman_viv/run.py \
|
||||
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.
|
||||
|
||||
@@ -22,6 +22,7 @@ from research.steady_pinball.run import Structure, config, scaled_points, CENTRE
|
||||
BIAS = np.array([0., -4., 4.])
|
||||
MODEL_SHA = '148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d'
|
||||
NORM_SHA = '534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0'
|
||||
RELEASE_SOURCE = '516975ef6482f983ace347cb10de7450d2fbd680'
|
||||
STEP_COLUMNS = (*COLUMNS, 'omega_F', 'omega_U', 'omega_L')
|
||||
|
||||
|
||||
@@ -161,12 +162,12 @@ def precursor(cfgpath, scale, warm, si, targets, out, start, cap):
|
||||
return seed, force
|
||||
|
||||
|
||||
def run_arm(name, cfgpath, scale, si, intervals, seed, initial_force, model, norm, out, start, cap):
|
||||
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(2., 1.e-4/scale**2, 0., diameter)
|
||||
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,
|
||||
@@ -244,16 +245,45 @@ def run_arm(name, cfgpath, scale, si, intervals, seed, initial_force, model, nor
|
||||
(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()
|
||||
@@ -272,14 +302,24 @@ def main():
|
||||
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., columns=STEP_COLUMNS)
|
||||
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 = precursor(cfgpath,scale,warm,si,targets,out,start,args.wall_seconds)
|
||||
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)
|
||||
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}'
|
||||
|
||||
@@ -68,14 +68,15 @@ def test_observation_clipping_and_nonfinite():
|
||||
m.observation(np.full(6,np.nan),np.zeros(6),2,norm)
|
||||
|
||||
|
||||
def test_arm_step_order_and_native_rhs(tmp_path):
|
||||
@pytest.mark.parametrize('mass_ratio', [2.,4.])
|
||||
def test_arm_step_order_and_native_rhs(tmp_path, mass_ratio):
|
||||
import CelerisLab
|
||||
from CelerisLab.body.motion import MovingTables
|
||||
from contextlib import nullcontext
|
||||
log=[];candidates=[]
|
||||
log=[];candidates=[];structure_args=[]
|
||||
class Structure:
|
||||
y=v=0.;mass=2.;stiffness=3.;frequency=.1
|
||||
def __init__(self,*args): pass
|
||||
def __init__(self,*args): structure_args.append(args)
|
||||
def candidate(self,f):
|
||||
candidates.append(f)
|
||||
return self.y+.001,self.v+.002
|
||||
@@ -100,7 +101,8 @@ def test_arm_step_order_and_native_rhs(tmp_path):
|
||||
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)
|
||||
m.run_arm('controlled','cfg',2,2,2,None,7.,model,norm,tmp_path,0,1,mass_ratio=mass_ratio)
|
||||
assert structure_args == [(mass_ratio,2.5e-5,0.,40.)]
|
||||
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
|
||||
@@ -113,3 +115,69 @@ def test_arm_step_order_and_native_rhs(tmp_path):
|
||||
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
|
||||
|
||||
|
||||
def test_mass4_changes_inertia_and_stiffness_not_frequency_or_damping():
|
||||
a=m.Structure(2,2.5e-5,0,40);b=m.Structure(4,2.5e-5,0,40)
|
||||
assert b.mass==2*a.mass and b.stiffness==2*a.stiffness
|
||||
assert b.frequency==a.frequency and b.damping==a.damping==0
|
||||
np.testing.assert_allclose(b.candidate(.001),np.asarray(a.candidate(.001))/2,rtol=0,atol=0)
|
||||
|
||||
|
||||
def make_release(path):
|
||||
import json
|
||||
(path/'zero').mkdir()
|
||||
case=dict(source=m.RELEASE_SOURCE,model_sha256=m.MODEL_SHA,norm_sha256=m.NORM_SHA,
|
||||
smoke=False,D=40,U=.005,nu=.004,warmup_each=2048000,target_intervals=150,SI=3200)
|
||||
(path/'case.json').write_text(json.dumps(case))
|
||||
(path/'config.json').write_text(json.dumps(m.config(40)))
|
||||
seed=(np.arange(18,dtype=np.float32),np.arange(18,dtype=np.float32)+1,np.zeros(2,np.int32))
|
||||
np.savez(path/'release.npz',current=seed[0],spare=seed[1],flags=seed[2],clock=4576000,force=.003)
|
||||
np.save(path/'target.npy',np.zeros((150,6),np.float32))
|
||||
transfer=dict(zip(('current','spare','flags'),map(m.array_sha,seed)),clock=4576000)
|
||||
(path/'zero/report.json').write_text(json.dumps(dict(release_transfer=transfer)))
|
||||
return seed
|
||||
|
||||
|
||||
@pytest.mark.parametrize('damage', [None,'source','config','buffer','target','clock'])
|
||||
def test_saved_release_checks_provenance_and_exact_inputs(tmp_path, damage):
|
||||
import json
|
||||
seed=make_release(tmp_path)
|
||||
if damage in ('source','config'):
|
||||
file=tmp_path/('case.json' if damage=='source' else 'config.json')
|
||||
data=json.loads(file.read_text());data['source' if damage=='source' else 'extra']='changed'
|
||||
file.write_text(json.dumps(data))
|
||||
elif damage in ('buffer','clock'):
|
||||
with np.load(tmp_path/'release.npz') as z: data={k:z[k] for k in z.files}
|
||||
if damage=='buffer': data['current'][0]+=1
|
||||
else: data['clock']=4576001
|
||||
np.savez(tmp_path/'release.npz',**data)
|
||||
elif damage=='target': np.save(tmp_path/'target.npy',np.zeros((149,6),np.float32))
|
||||
if damage:
|
||||
with pytest.raises(ValueError): m.saved_release(tmp_path)
|
||||
else:
|
||||
loaded,force=m.saved_release(tmp_path)
|
||||
assert loaded[3]==4576000 and force==.003
|
||||
for actual,expected in zip(loaded[:3],seed): np.testing.assert_array_equal(actual,expected)
|
||||
|
||||
|
||||
def test_mass4_cli_uses_saved_release_without_warmup(tmp_path, monkeypatch):
|
||||
import json
|
||||
original=tmp_path/'original';original.mkdir();make_release(original)
|
||||
norm=tmp_path/'norm.json';norm.write_text('{}');model=tmp_path/'model.zip'
|
||||
out=tmp_path/'output';calls=[]
|
||||
monkeypatch.setattr(m.sys,'argv',['run.py','--model',str(model),'--norm',str(norm),
|
||||
'--out',str(out),'--mass-ratio','4','--release-dir',str(original)])
|
||||
monkeypatch.setattr(m,'sha',lambda path:m.MODEL_SHA if path==model else m.NORM_SHA if path==norm else 'fixture-sha')
|
||||
monkeypatch.setattr(m,'load_policy',lambda path:object())
|
||||
monkeypatch.setattr(m.subprocess,'check_output',lambda *a,**k:'candidate\n')
|
||||
monkeypatch.setattr(m,'precursor',lambda *a:pytest.fail('saved release must skip precursor'))
|
||||
monkeypatch.setattr(m,'run_arm',lambda *a,**k:calls.append((a,k)))
|
||||
m.main()
|
||||
assert [a[0] for a,k in calls]==['zero','controlled']
|
||||
assert all(a[2:5]==(2.,3200,400) and k=={'mass_ratio':4.} for a,k in calls)
|
||||
receipt=json.loads((out/'case.json').read_text())
|
||||
assert receipt['mass_ratio']==4 and receipt['structural_damping']==0 and receipt['warmup_executed']==0
|
||||
assert receipt['exploratory'] and receipt['response_tU_D']==160
|
||||
assert (out/'target.npy').read_bytes()==(original/'target.npy').read_bytes()
|
||||
assert (out/'config.json').read_bytes()==(original/'config.json').read_bytes()
|
||||
|
||||
Reference in New Issue
Block a user