Add one predeclared mass-four paired VIV exploration

This commit is contained in:
Frank14f
2026-11-01 00:35:34 +08:00
parent 516975ef64
commit 73d3f6214e
3 changed files with 154 additions and 9 deletions
+37
View File
@@ -81,3 +81,40 @@ CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src python research/karman_viv/run.py \
First `--smoke`: same D40 geometry, warmup32/target2x8/warmup32, First `--smoke`: same D40 geometry, warmup32/target2x8/warmup32,
4x8 response steps each arm; strictly plumbing only. CPU source checks and 4x8 response steps each arm; strictly plumbing only. CPU source checks and
independent exact-tip review precede GPU. Preserve original DynamisLab files. 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.
+45 -5
View File
@@ -22,6 +22,7 @@ from research.steady_pinball.run import Structure, config, scaled_points, CENTRE
BIAS = np.array([0., -4., 4.]) BIAS = np.array([0., -4., 4.])
MODEL_SHA = '148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d' MODEL_SHA = '148240c1dcb8b11d8e5a0a9e991f6874c4e799c9744707bf0fb32a8efeb0468d'
NORM_SHA = '534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0' NORM_SHA = '534fda818c030a56960e95d0f3ac1e350d887d03db7f80f1d6453195a502b7f0'
RELEASE_SOURCE = '516975ef6482f983ace347cb10de7450d2fbd680'
STEP_COLUMNS = (*COLUMNS, 'omega_F', 'omega_U', 'omega_L') 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 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 import Simulation
from CelerisLab.body.motion import MovingTables from CelerisLab.body.motion import MovingTables
cols = {k:i for i, k in enumerate(MovingTables.DIAG)} cols = {k:i for i, k in enumerate(MovingTables.DIAG)}
diameter = 20.*scale 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 = out/name
arm.mkdir() arm.mkdir()
rows = np.lib.format.open_memmap(arm/'steps.npy', mode='w+', dtype=np.float64, 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') (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(): def main():
ap = argparse.ArgumentParser(description=__doc__) ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--model', type=Path, required=True) ap.add_argument('--model', type=Path, required=True)
ap.add_argument('--norm', type=Path, required=True) ap.add_argument('--norm', type=Path, required=True)
ap.add_argument('--out', 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('--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.) ap.add_argument('--wall-seconds', type=float, default=14400.)
args = ap.parse_args() args = ap.parse_args()
if not np.isfinite(args.wall_seconds) or args.wall_seconds<=0: if not np.isfinite(args.wall_seconds) or args.wall_seconds<=0:
ap.error('positive finite wall cap required') 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: if sha(args.model)!=MODEL_SHA or sha(args.norm)!=NORM_SHA:
ap.error('model/normalizer must match frozen provenance') ap.error('model/normalizer must match frozen provenance')
out = args.out.resolve() out = args.out.resolve()
@@ -272,14 +302,24 @@ def main():
response_steps=si*intervals,response_tU_D=si*intervals*.005/40, 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', 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', 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') (out/'case.json').write_text(json.dumps(receipt,indent=2)+'\n')
start = time.monotonic() start = time.monotonic()
status = 'running' status = 'running'
try: 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'): 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' status = 'smoke_completed' if args.smoke else 'pair_completed_not_qualified'
except BaseException as exc: except BaseException as exc:
status = f'error: {type(exc).__name__}: {exc}' status = f'error: {type(exc).__name__}: {exc}'
+72 -4
View File
@@ -68,14 +68,15 @@ def test_observation_clipping_and_nonfinite():
m.observation(np.full(6,np.nan),np.zeros(6),2,norm) 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 import CelerisLab
from CelerisLab.body.motion import MovingTables from CelerisLab.body.motion import MovingTables
from contextlib import nullcontext from contextlib import nullcontext
log=[];candidates=[] log=[];candidates=[];structure_args=[]
class Structure: class Structure:
y=v=0.;mass=2.;stiffness=3.;frequency=.1 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): def candidate(self,f):
candidates.append(f) candidates.append(f)
return self.y+.001,self.v+.002 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), \ 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,'add_scene',return_value=([1,2,3],[4,5,6])),patch.object(m,'install_seed',return_value={}), \
patch.object(m,'check_time'): 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 candidates==[7.,33.,66.,99.] # previous native force, NOT SI mean or node impulse
assert not seen[0].any() and len(seen)==2 assert not seen[0].any() and len(seen)==2
assert [e[0] for e in log]==['set','set','set','run']*4 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 np.testing.assert_array_equal(rows[0,15:],rows[2,15:]) # EMA resets at SI boundary
signals=np.load(tmp_path/'controlled/signals.npz') signals=np.load(tmp_path/'controlled/signals.npz')
np.testing.assert_array_equal(signals['forces'][:,1],[3.,7.]) # interval CME *2, not CME+Wen 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()