Predeclare zero-damping pinball development cases

This commit is contained in:
Frank14f committed 2026-10-31 15:51:39 +08:00
1 parent d0bcefa60c
commit 7b81584458
3 files changed
+83 -12

No files matched your search

+32 -1
View File
@@ -5,7 +5,8 @@ rho=1, mean U=.01, nu=.004, D=20, Re_D=50. Centres are (600,255.5),
(626,270.5), (626,240.5), radius10. No upstream cylinder. Passive probes stay (626,270.5), (626,240.5), radius10. No upstream cylinder. Passive probes stay
at (800,295.5), (800,255.5), (800,215.5), radius5. Every omega is zero. at (800,295.5), (800,255.5), (800,215.5), radius5. Every omega is zero.
Parabolic inlet, regularized NEQ damping1; y bounce-back planes .5/510.5; Parabolic inlet, regularized NEQ damping1; y bounce-back planes .5/510.5;
NEQ-extrapolated outlet with existing backflow clamp/blending. Initial fluid is NEQ-extrapolated outlet with backflow clamp (the configured blend is inactive in
this MRT route). Initial fluid is
parabolic equilibrium. Fixed warmup is 4 NX/U=512000 steps. parabolic equilibrium. Fixed warmup is 4 NX/U=512000 steps.
Geometry and fluid parameters follow DynamisLab 609116ec26f19766f5e9ce2b0df427d4e24faae8, Geometry and fluid parameters follow DynamisLab 609116ec26f19766f5e9ce2b0df427d4e24faae8,
@@ -71,3 +72,33 @@ This stage makes no claim of physical grid convergence or vibration suppression.
Run from checkout with PYTHONPATH=src and the existing Python environment: Run from checkout with PYTHONPATH=src and the existing Python environment:
`python research/steady_pinball/run.py --smoke --out <new-external-directory>` `python research/steady_pinball/run.py --smoke --out <new-external-directory>`
then separately `--out <new-free-directory>` and `--fixed --out <new-fixed-directory>`. then separately `--out <new-free-directory>` and `--fixed --out <new-fixed-directory>`.
## Zero-damping development follow-up (predeclared 2026-10-31)
The original d0bcefa run and its zeta=.01 results remain historical evidence.
The current runner exposes three fixed `--case` choices, all with C=0 as requested:
| Case | M / (rho 3 pi D^2/4) | fn | M | K | Purpose |
|---|---:|---:|---:|---:|---|
| Z0 | 10 | .0001 | 9424.77796 | .00372075320 | Remove damping only |
| M2 | 2 | .0001 | 1884.95559 | .000744150640 | Reduce mass and stiffness together at fixed fn |
| R2 | 2 | .00006 | 1884.95559 | .000267894231 | Change stiffness only relative to M2 |
R2 deliberately places fn at the retained fixed-body lift peak, f=.00006
(St=.12 on mean U). This is an informed development choice, not independent
physical validation or a promise of lock-in. All geometries, fluid settings,
512000-step fixed warmup, y0=v0=0, force time level and unfiltered RHS are unchanged.
No run result may select a replacement parameter or an automatic extension.
Each case has a 600000-step cap, the same 300 D/U physical duration. This equals
60 natural periods for Z0/M2 and 36 for R2. Compare the common final200000 native
steps, and six100000-step blocks, retaining actual natural/response frequencies
and ongoing drift. Do not call those blocks ten R2 periods. No settled-VIV claim
follows from a larger displacement. Report all three outcomes, including quiet
responses or stopped cases, alongside the existing fixed and damped-free records.
Unchanged stops: |y|>D, invalid move/speed, nonfinite values or no_donor. A 1800s
per-case wall cap and GPU headroom check bound resource use. Preserve partial rows;
do not add damping, clip trajectories, filter forces, or relax any stop. Each
case adds72MB of scalar history, no full fields. The separate Legacy/modern
baseline diagnosis does not change this FSI protocol.
+20 -10
View File
@@ -1,6 +1,6 @@
"""Uncontrolled paper-geometry steady Re_D=50, one transverse assembly DOF. """Uncontrolled paper-geometry steady Re_D=50, one transverse assembly DOF.
One fixed protocol; --smoke performs 32 warmup + 32 observation steps only. Three predeclared zero-damping structures; --smoke uses only 32 + 32 steps.
See docs/steady_pinball_fiv.md for historical solver differences and new structure. See docs/steady_pinball_fiv.md for historical solver differences and new structure.
""" """
from __future__ import annotations from __future__ import annotations
@@ -18,6 +18,7 @@ D, U, NU = 20., .01, .004
CENTRES = ((600., 255.5), (626., 270.5), (626., 240.5)) CENTRES = ((600., 255.5), (626., 270.5), (626., 240.5))
PROBES = ((800., 295.5), (800., 255.5), (800., 215.5)) PROBES = ((800., 295.5), (800., 255.5), (800., 215.5))
WARMUP, STEPS, PERIOD = 512000, 600000, 10000 WARMUP, STEPS, PERIOD = 512000, 600000, 10000
STRUCTURES = {'Z0': (10., 1.e-4), 'M2': (2., 1.e-4), 'R2': (2., 6.e-5)}
COLUMNS = ('step', 'y', 'v', 'cme_F', 'cme_U', 'cme_L', 'wen_F', 'wen_U', 'wen_L', COLUMNS = ('step', 'y', 'v', 'cme_F', 'cme_U', 'cme_L', 'wen_F', 'wen_U', 'wen_L',
'node_F', 'node_U', 'node_L', 'eq_only', 'no_donor', 'crossings') 'node_F', 'node_U', 'node_L', 'eq_only', 'no_donor', 'crossings')
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
@@ -29,10 +30,15 @@ def config():
class Structure: class Structure:
"""Exact damped oscillator update for a force held over dt=1; no force filter.""" """Exact damped oscillator update for a force held over dt=1; no force filter."""
def __init__(self): def __init__(self, mass_ratio=10., frequency=1.e-4, zeta=0.):
self.mass = 10. * 3. * math.pi * D * D / 4. if not (math.isfinite(mass_ratio) and mass_ratio > 0. and
self.omega = 2. * math.pi / PERIOD math.isfinite(frequency) and frequency > 0. and
self.zeta = .01 math.isfinite(zeta) and 0. <= zeta < 1.):
raise ValueError('positive finite mass ratio/frequency and 0 <= zeta < 1 required')
self.mass_ratio, self.frequency = mass_ratio, frequency
self.mass = mass_ratio * 3. * math.pi * D * D / 4.
self.omega = 2. * math.pi * frequency
self.zeta = zeta
self.stiffness = self.mass * self.omega**2 self.stiffness = self.mass * self.omega**2
self.damping = 2. * self.zeta * self.mass * self.omega self.damping = 2. * self.zeta * self.mass * self.omega
self.y = self.v = 0. self.y = self.v = 0.
@@ -68,6 +74,8 @@ def main():
ap.add_argument('--out', required=True, type=Path) ap.add_argument('--out', required=True, type=Path)
ap.add_argument('--fixed', action='store_true', help='same modern moving-path geometry held fixed') ap.add_argument('--fixed', action='store_true', help='same modern moving-path geometry held fixed')
ap.add_argument('--smoke', action='store_true', help='32+32 steps, no physical result') ap.add_argument('--smoke', action='store_true', help='32+32 steps, no physical result')
ap.add_argument('--case', choices=STRUCTURES, default='Z0',
help='predeclared structure; all cases have zero structural damping')
args = ap.parse_args() args = ap.parse_args()
out = args.out.resolve() out = args.out.resolve()
if out == ROOT or ROOT in out.parents: if out == ROOT or ROOT in out.parents:
@@ -76,7 +84,7 @@ def main():
cfg = config() cfg = config()
(out/'config.json').write_text(json.dumps(cfg, indent=2)+'\n') (out/'config.json').write_text(json.dumps(cfg, indent=2)+'\n')
source = subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() source = subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip()
s = Structure() s = Structure(*STRUCTURES[args.case], zeta=0.)
warmup, count = (32, 32) if args.smoke else (WARMUP, STEPS) warmup, count = (32, 32) if args.smoke else (WARMUP, STEPS)
rows = np.lib.format.open_memmap(out/'steps.npy', mode='w+', dtype=np.float64, rows = np.lib.format.open_memmap(out/'steps.npy', mode='w+', dtype=np.float64,
shape=(count, len(COLUMNS))) shape=(count, len(COLUMNS)))
@@ -87,11 +95,13 @@ def main():
col = {k: names.index(k) for k in names} col = {k: names.index(k) for k in names}
status, completed, f_last = 'running', 0, 0. status, completed, f_last = 'running', 0, 0.
started = time.monotonic() started = time.monotonic()
report = dict(source=source, fixed=args.fixed, smoke=args.smoke, columns=COLUMNS, report = dict(source=source, case=args.case, fixed=args.fixed, smoke=args.smoke, columns=COLUMNS,
structure=dict(mass=s.mass, stiffness=s.stiffness, damping=s.damping, structure=dict(mass=s.mass, stiffness=s.stiffness, damping=s.damping,
zeta=s.zeta, fn=1./PERIOD, y0=0., v0=0.), mass_ratio_three_disks=s.mass_ratio,
zeta=s.zeta, fn=s.frequency, Tn=1./s.frequency, y0=0., v0=0.),
warmup_steps=warmup, requested_steps=count, dt=1., D=D, U=U, nu=NU, warmup_steps=warmup, requested_steps=count, dt=1., D=D, U=U, nu=NU,
duration_Tn=count/PERIOD, duration_tU_D=count*U/D, duration_Tn=count*s.frequency, duration_tU_D=count*U/D,
summary_block_steps=10*PERIOD,
rhs='sum of three instantaneous CME_y + Wen_y, one-step lag, no filtering') rhs='sum of three instantaneous CME_y + Wen_y, one-step lag, no filtering')
try: try:
with Simulation(str(out/'config.json')) as sim: with Simulation(str(out/'config.json')) as sim:
@@ -135,7 +145,7 @@ def main():
f_last = float(cme.sum(dtype=np.float64)+wen.sum(dtype=np.float64)) f_last = float(cme.sum(dtype=np.float64)+wen.sum(dtype=np.float64))
if completed % PERIOD == 0: if completed % PERIOD == 0:
rows.flush() rows.flush()
print(json.dumps(dict(event='period', step=completed, t_Tn=completed/PERIOD, print(json.dumps(dict(event='progress', step=completed, t_Tn=completed*s.frequency,
y_D=s.y/D, elapsed_s=time.monotonic()-started)), flush=True) y_D=s.y/D, elapsed_s=time.monotonic()-started)), flush=True)
else: else:
status = 'smoke_completed' if args.smoke else 'duration_completed_not_qualified' status = 'smoke_completed' if args.smoke else 'duration_completed_not_qualified'
+31 -1
View File
@@ -26,7 +26,7 @@ def test_actual_loader_and_paper_geometry(tmp_path):
def test_structure_rest_equilibrium_and_energy_decay(): def test_structure_rest_equilibrium_and_energy_decay():
s = runner.Structure() s = runner.Structure(zeta=.01)
assert s.candidate(0.) == (0., 0.) assert s.candidate(0.) == (0., 0.)
assert s.mass == pytest.approx(10*3*math.pi*100) assert s.mass == pytest.approx(10*3*math.pi*100)
s.y, s.v = .2, 0. s.y, s.v = .2, 0.
@@ -52,3 +52,33 @@ def test_report_uses_completed_rows_and_net_force():
assert got[0]['end_step'] == 4 assert got[0]['end_step'] == 4
assert got[0]['y_rms_D'] == .1 assert got[0]['y_rms_D'] == .1
assert got[0]['fy_rms'] == 6. assert got[0]['fy_rms'] == 6.
@pytest.mark.parametrize('case', ['Z0', 'M2', 'R2'])
def test_zero_damping_cases_conserve_unforced_energy_and_match_forced_solution(case):
s = runner.Structure(*runner.STRUCTURES[case])
assert s.zeta == s.damping == 0.
s.y, s.v = .2, 1.e-5
y0, v0 = s.y, s.v
energy = .5*s.mass*v0**2 + .5*s.stiffness*y0**2
for _ in range(12345):
s.y, s.v = s.candidate(0.)
assert .5*s.mass*s.v**2 + .5*s.stiffness*s.y**2 == pytest.approx(energy, rel=2e-11)
force, elapsed = 1.e-4, 3210
s.y, s.v = y0, v0
for _ in range(elapsed):
s.y, s.v = s.candidate(force)
phase = s.omega*elapsed
equilibrium = force/s.stiffness
expected_y = equilibrium+(y0-equilibrium)*math.cos(phase)+v0*math.sin(phase)/s.omega
expected_v = -(y0-equilibrium)*s.omega*math.sin(phase)+v0*math.cos(phase)
assert (s.y, s.v) == pytest.approx((expected_y, expected_v), abs=2e-11)
def test_predeclared_comparisons_change_structure_only():
z, m, r = [runner.Structure(*runner.STRUCTURES[k]) for k in ['Z0', 'M2', 'R2']]
assert m.mass/z.mass == pytest.approx(.2)
assert m.stiffness/z.stiffness == pytest.approx(.2)
assert r.mass == m.mass
assert r.stiffness/m.stiffness == pytest.approx(.36)
assert [runner.STEPS*s.frequency for s in [z, m, r]] == pytest.approx([60., 60., 36.])