CaltechExperimentalGravity / CaltechExperimentalGravity/system_ident

Segmented ArbitraryStream excitation with a cosine on/off envelope; ArbitraryLoop as a peer mode

Open
#31 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Python
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Description

Supersedes the design consequence of #9. Design detail: spec **§2.3** (new), with the two corrections it rests on boxed in **§2.2** and **§8b**.

## Why

`_soft_start_stop` (`backends/base.py`) is `scipy.signal.windows.tukey` — a **cosine** taper, C¹, slope starts at zero. `ArbitraryLoop.start(ramptime)` is `set_gain(0)` then `set_gain(g, ramptime=…)` → `awgSetGain` — a **linear** gain ramp with a slope discontinuity at both ends. The 2026-08-04 operator run measured that linearity to five decimals (#27).

**A cosine taper is gentler on the actuator and is therefore preferred.** So "use the transport's own ramp", which #9's diagnosis implied, is an envelope-quality *downgrade*, not a neutral swap. It was only acceptable because a *looped* array leaves no alternative: tapering the staged array turns the taper into periodic AM and the seam re-excites the plant every cycle — measured **2.81e-1** FRF error (spec §2.2).

## What

Build the entire excitation as **one array carrying a cosine on/off envelope** and inject it once with **`awg.ArbitraryStream`**, in four temporal segments:

| # | segment | duration | envelope | analysed? |
|---|---|---|---|---|
| 1 | **ramp-on** | `t_ramp` | cosine, rising | no |
| 2 | **settle** — the system under test's transient decays | `warmup_s` | flat, full amplitude | **no, discarded** |
| 3 | **main excitation** — read back, FRF measured | `segment_duration × n_segments` | flat, full amplitude | **yes** |
| 4 | **ramp-off** | `t_ramp` | cosine, falling | no |

Segments 1, 2 and 4 sit **outside** the analysed window, so the taper never touches the periods entering the FRF and segment 3 stays exactly periodic. `read()` must window **segment 3 only** (#14), and a config check should enforce that `t_ramp` fits the lead it tapers into.

**No new config keys.** The four durations derive from keys that already exist and already carry this meaning in the twin and `rtsfreerun` backends: `t_ramp`, `warmup_s`, `segment_duration`, `n_segments`. One meaning per knob across all backends. Shipped `configs/rtsfreerun_hsts.yml` gives `3 + 24 + 96 + 3 = 126 s`.

## Two findings that make this viable

Both verified read-only against `awg` 4.1.4 on the deployment machine:

1. **`ArbitraryStream` exists, and `ArbitraryLoop` subclasses it** — `class ArbitraryLoop(ArbitraryStream)`. Available: `open()`, `append(data, scale)`, `close()`, `send()`, `abort()` → `SIStrAbort`, `set_gain(gain, ramptime, wait)` → `awgSetGain`.
2. **A blocking claim in §2.2 is false.** It argued against the one-shot construction because *"`ramp_down` would have to rewrite an already-queued array, which is impossible."* Not so: `set_gain` and `abort` are **independent of the queued data**. So `ramp_down(channel, secs) → set_gain(0, ramptime=secs)` maps exactly (#16), and #19's mandatory out-of-band STOP gets `abort()`.

Note §2.2's own table already scored **one-shot lead+record+tail at 8.8e-12** — as good as the `ramptime` path (1.4e-11). The preferred construction was measured all along, and it is the `RTSfreerunBackend` construction, so **twin and hardware share one envelope construction instead of two**.

## `ArbitraryLoop` stays a first-class peer — `cds.exc_mode: stream | loop`

Not a deprecated path. Both modes supported, both tested, both in the #5 fake harness. `exc_mode` defaults to **`stream`**; choosing `loop` is a deliberate config choice and **emits a warning** naming the consequence — linear envelope, harsher on the actuator.

`loop`'s four roles:

1. **Fallback** if stream injection misbehaves on hardware.
2. **Development pipeline** — simpler to drive.
3. **Valid on request**, or wherever a linear ramp carries no disadvantage for the use case.
4. **Very long excitations** — with the justification corrected below.

### Role 4, computed rather than asserted

Per the feasibility gate in `CLAUDE.md` — no claimed limit without the number:

- Shipped config, one experiment = 126 s. At `fs_hw = 16384`, float64: `126 × 16384 × 8` ≈ **16.5 MB**. Memory is a non-issue.
- **Q-limited** physics-sized record: `T ≥ ~Q/f0` gives `T_perseg ≈ 1493 s` for Q = 1000 at 0.67 Hz, so 6 periods ≈ 8955 s ≈ 2.5 h → **≈1.17 GB per channel**, ≈3.5 GB for 3 DoF simultaneous. Large, still tractable, and campaign estimates already run to hours.
- **But `append` can be fed in chunks.** The segmented envelope needs only the total *length* up front, not the samples, so the array need never be resident — it can be generated period by period. **The memory rationale dissolves.**
- **Stream underrun does not dissolve.** `append` pushes data in real time from Python; a stall (GC, NFS, network) starves the stream and leaves a **gap in the excitation**. `ArbitraryLoop` is structurally immune — the front end repeats a staged buffer with no further Python involvement.

So **role 4 is justified on underrun-immunity, not memory.** Stronger argument, and it scales with record duration rather than being a remote edge case.

### Underrun must be detected explicitly — it is a #8-class fault

A gap in the excitation is exactly what `H_err` and coherence **cannot see** (spec §3.3: they are period-to-period scatter, so a corrupted FRF can still report coherence ≈ 1). Reuse the two detections that already exist rather than inventing a third:

1. **#8's independent check** — measured `Xbar` phase vs the known injected multisine phase.
2. **Compare the `_EXC` readback against the commanded array.** Newly known feasible: §8b measured the excitation readback as NDS-readable at the model rate, round-tripping amplitude and shape exactly. A gap shows up directly.

## Work items

- [ ] `#5` — add `FakeArbitraryStream` recording `append` sizes/order so chunking and underrun are testable; keep `FakeArbitraryLoop`.
- [ ] `#11` — transport seam expresses **both** modes.
- [ ] `#13` — stream mode builds the segmented enveloped array; the `_soft_start_stop` pass-through override applies to **loop mode only**.
- [ ] `#14` — analysed read window = **segment 3 only**.
- [ ] `#15` — segment 2 *is* the settle.
- [ ] `#16` — `set_gain(0, ramptime=…)` graceful, `abort()` hard; underrun handling in the `finally`.
- [ ] `#19` — `abort()` as the STOP path on the stream.
- [ ] `#20` — `exc_mode` in the config schema and `--cds` wiring, with the loop-mode warning.
- [ ] `base.py` ramp contract — keep it **two-branch**, but each branch must state **which envelope shape it produces** and that cosine is preferred. That omission is what let the two be treated as equivalent.
- [ ] Rework spec **§4.1 / §4.2**, which still specify `ArbitraryLoop` as the only transport.

## Deferred — planned, not implemented

- **Shrinking segment 2, via #1** (Local Polynomial Method / GraFIT). LPM estimates the transient/leakage term instead of waiting it out, so the discarded settle — the dominant fixed cost per experiment — could shrink. **Defer planning.**
- **MIMO.** Segments 1–4 constitute one **'experiment'**; a MIMO measurement contains one experiment **per input**, so `n_experiments = n_inputs`. **Defer planning.**

## Not in scope

Any hardware injection. `ArbitraryStream` has never been driven at the site — see #27 for the stream-specific unknowns. Every injection needs its own separate operator approval regardless of what came before.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with backends/base.py, the shipped configs/rtsfreerun_hsts.yml, and the linked work items #5, #11, #13, #14, #16, #19, and #20. Check the existing fake harness and stream/loop entry points before defining the segmented stream path, analysed window, stop handling, and config wiring. Done means both modes remain supported and the specified tests and underrun checks cover their distinct envelope behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.