# PFR rig: all real measurements, 2026-10-12

Every file here is real rig data. Nothing is simulated. The 2026-10-11 `*_speed` / `*_distance` folders one level up were FakeRig simulations, so they are not included.

**Units.** Force is in raw HX711 counts. `counts/g` was never calibrated (routine B not run), so every "gf" number uses the guess **100 counts/g**. Real grams could be off by a factor of about 2–4. Raw counts are always kept, so you can rescale later.

**Start here for the 1.12 in campaign:** [SYNOPSIS_gap1.12_measurement_run3.md](SYNOPSIS_gap1.12_measurement_run3.md): context, history, methods and file guide (its result numbers are superseded by the audit corrections below).

## 2026-10-18 audit corrections (read first)

- **Every force is "sign unverified".** Nobody has yet checked which way is "pushed away from the rotor". **K is shown as "--"** until the new Sign check and calibration B are both done. **Every K value elsewhere in this folder is withdrawn.**
- **Withdrawn:** the "cool cycles (A power ≤ 61 %)" result and its K = 96 headline (a post-hoc selection), plus the eddy-corrected nets, K values and polarity split built on it. Those summary files are in `withdrawn/` with a note.
- **Measurement run 1 and control run 1 are back in every table**, flagged ("before refit", "noisy"), not excluded.
- **New primary statistic** (pre-registered for every run from now on, `prereg.json` in each run folder): **A-B-A dP** from 1 s block medians, instrument-fault filter only, 7 s discard, all valid cycles, **no eddy correction**. Secondary: plain dP, full-half dP, eddy-model value (shown, never subtracted).
- The old runs below had no pre-registration. Their numbers are the new primary statistic applied **retrospectively** (`tools/pfr/pfr_retro.py`), so they are exploratory. Gap 1.12 in, gf at the guessed 100 counts/g, sign unverified:

| Label | Run | A-B-A dP (PRIMARY) | plain dP | full-half dP | eddy model (not subtracted) | valid cycles | Flag |
|---|---|---|---|---|---|---|---|
| measurement | `legacy_20261015_181034_measurement` | -0.822 ± 0.091 | -0.982 | -0.459 | -0.054 | 7/7 | before refit; retrospective, no prereg |
| measurement | `legacy_20261015_195257_measurement` | +0.232 ± 0.017 | +0.293 | +0.230 | -0.073 | 7/7 | retrospective, no prereg |
| measurement | `legacy_20261015_202137_measurement` | +0.212 ± 0.046 | +0.382 | +0.330 | -0.070 | 7/7 | retrospective, no prereg |
| measurement | **all 3 runs pooled** | **-0.126 ± 0.348** | | | | | mean of run values, between-run SE |
| control: magnet flipped | `legacy_20261015_210526_magnet-flipped` | -1.013 ± 0.037 | -1.014 | -0.794 | -0.077 | 5/5 | retrospective, no prereg |
| control: magnet flipped | `legacy_20261015_211136_magnet-flipped` | -0.918 ± 0.015 | -0.875 | -0.716 | -0.062 | 5/5 | retrospective, no prereg |
| control: magnet flipped | **all 2 runs pooled** | **-0.965 ± 0.047** | | | | | mean of run values, between-run SE |
| control: rotor magnet removed | `legacy_20261015_185955_rotor-magnet-removed` | -0.682 ± 0.181 | -0.639 | -0.543 | +0.000 | 7/7 | noisy; retrospective, no prereg |
| control: rotor magnet removed | `legacy_20261015_192011_rotor-magnet-removed` | +0.013 ± 0.015 | +0.030 | -0.040 | +0.000 | 7/7 | retrospective, no prereg |
| control: rotor magnet removed | `legacy_20261015_192555_rotor-magnet-removed` | -0.032 ± 0.019 | -0.025 | -0.040 | +0.000 | 7/7 | retrospective, no prereg |
| control: rotor magnet removed | `legacy_20261015_193215_rotor-magnet-removed` | -0.113 ± 0.020 | -0.079 | -0.024 | +0.000 | 7/7 | retrospective, no prereg |
| control: rotor magnet removed | **all 4 runs pooled** | **-0.203 ± 0.162** | | | | | mean of run values, between-run SE |

Pooled with run 1 included, the measurement is −0.13 ± 0.35 gf: the runs disagree. Flipped runs vs the pooled measurement: DIFFERENT (exploratory). All runs: `logs/pfr/chop_runs.json`; flags: `run_flags.json`; standalone check: `tools/pfr/audit_chops.py`.


## Folders

| # | Folder | Gap | What it is |
|---|---|---|---|
| 00 | `00_other_sessions_raw` | various | **Every other real session, as recorded** (added 2026-10-19 for completeness): the early dashboard and development sessions of 2026-10-11 and 10-12 (older firmware: `raw.csv` has fewer columns, e.g. `host_time,t_ms,force_raw,f_Hz[,motor_pct,...]`; the speed column is NOT a real rotor speed before the coil sensor of 10-12 16:00), short 10-15 sessions (dashboard restarts, spin checks, the replug at 21:01–21:02), and the failed 10-15 20:58 attempt (no board data; its screen video `attempt_20261015_205806_magnet-flipped_no-board-data.mp4`). Some folders hold only an `events.log`. Not included anywhere: the 10-11 `*_speed` / `*_distance` folders (simulated, `fw: fake`). |
| 01 | `01_gap2.5_noise_and_power_sweep` | 2.5 in | C noise/drift (120 s, motor off), then fixed-POWER tare-run-tare sweep 50–80 % and 5× repeat at 60 %. Speed column here is NOT real (old SS49E speed). |
| 02 | `02_coil_speed_sensor` | 2.5 in | Proof that the coil (D2) sees the rotor: raw coil captures at 0/50/60/70/80 % (1953 values/s, ~1 s each) and the power-to-speed curve (40–100 %). |
| 03 | `03_gap2.5_speed_hold_tests` | 2.5 in | Speed-hold tuning runs (hold 45/50/54/70/80/95 Hz). Tests 2–4 include the failures (stall, oscillation), and 5–6 are the working hold. |
| 04 | `04_gap2.5_speed_hold_sweep` | 2.5 in | Speed-HELD tare-run-tare at 45/50/54 Hz (each 2×) + 50 Hz repeats. |
| 05 | `05_dashboard_session_1742_manual_motor` | 2.5 in | Your manual session: 19 saved points up and down 44→72→35 Hz (`points.csv`). Ended with ABORT. |
| 06 | `06_motor_off_72min_long_drift` | 0.8 in (from ~18:07) | 72 min with the motor off (the cool-down break). The magnet was moved to 0.8 in at the very start: `force_raw` steps from about −304,900 (17:36, folder 04) to −308,100 by 18:07. After that it's a clean long drift baseline: −308,112 → −308,338 over 70 min, about **−3 counts/min**. |
| 07 | `07_gap0.80_sweep_attempt_aborted` | 0.8 in | Up-down sweep routine, auto-range ramp only (2 % steps up to 100 %). Then the speed collapsed at 100 % (mount breaking, motor hot), and you pressed ABORT. `raw.csv` = 19:17:07–19:26:16, the whole test (start 19:18:19, ABORT 19:21:43) + 4.5 min motor off after; `raw_after_abort_motor_off_1926-1933.csv` = the next 7 min, motor off (after the dashboard restart); `autorange_ramp_steps.csv` = per-step averages I extracted from `raw.csv`. No sweep CSV: the routine was aborted before cycle 1 finished. |

| 08 | `08_gap0.80_chop_control_removed_70-100Hz_aborted` | 0.8 in, test magnet REMOVED | Speed chop A 70 / B 100 Hz (after the mount repair). Stopped after cycle 1: the rotor needed about 90 % for 70 Hz, so B couldn't be reached under the 90 % limit. |
| 09 | `09_gap0.80_chop_45-55Hz_control_and_measurement` | 0.8 in | One dashboard session, two speed-chop runs at A 45 / B 55 Hz (`raw.csv` holds both). **(a) 20:18–20:23, test magnet REMOVED** (`chop_magnet_removed_*`): 13 cycles, stopped because the motor was heating. Power for ~50 Hz climbed from 75 % to 88 % after ~3 min, and cycles 10–13 had no valid force blocks. **8 valid cycles: dP = −0.112 ± 0.029 gf, apparent K = −2.88 ± 0.74 1/kg = rig artifact.** **(b) 20:56–20:58, test magnet ON, normal** (`chop_measurement_*`): the rotor wouldn't keep turning at ~45 % with the magnet on (cogging; before the repair it needed ≥ 64 % at this gap). It stalled, the motor went off, and the run was aborted. No result. **(c) 21:02, last try (speed map, magnet ON):** at 70–74 % the rotor did not turn at all (speed 0, coil swing ~16 mV vs 20–30 mV when spinning). The force jumped about +2,600 counts when power was applied: the motor pulling the rotor magnet against the test magnet. The safety switched the motor off after 2 re-kicks. |

| 10 | `10_gap1.12_new_motor` | 1.12 in (2026-10-15, NEW motor) | `checks_motor_and_EMI`: new-motor checks and the EMI fixes (before: ~2 HX711 readings/s with the motor on and board restarts at low PWM frequency; after: ~10 readings/s). Speed chops at 65/85 Hz: measurement runs 1–3 (test magnet N facing the rotor; run 1 before the rotor magnet was refitted), rotor-magnet-removed controls 1–4 (power chop 53/77 %), flipped runs 1–2 (S facing the rotor), with videos for the flipped runs. **Results: see "2026-10-18 audit corrections" above.** The numbers this row used to quote (cool-cycle selection, K values, eddy-corrected nets, polarity split) are withdrawn. |
| 11 | `11_gap1.12_preregistered_flipped_and_measurement` | 1.12 in (2026-10-18/19) | **Pre-registered runs** in one dashboard session (`20261018_212330/`: `raw.csv`, `events.log`, one `run_*` folder per run with `prereg.json`, `cycles.csv`, `summary.txt`, video). Speed chop 65/85 Hz, 5 cycles, alternating order, 10 min cool-down. PRIMARY A-B-A dP (gf at 100 counts/g, sign unverified, no eddy correction): **S facing the rotor (flipped), 4 runs: −0.955 ± 0.056, −1.041 ± 0.106, −1.066 ± 0.088, −0.858 ± 0.044 → −0.980 ± 0.047**; **N facing the rotor (measurement), 4 runs: +0.919 ± 0.053, +0.572 ± 0.132, +1.016 ± 0.046, +1.196 ± 0.048 → +0.925 ± 0.131** (between-run SE). Exploratory: polarity-free part (N+S)/2 = −0.03 ± 0.07 gf; polarity-dependent part (N−S)/2 = +0.95 ± 0.07 gf. With S facing, A sat at 65–68 Hz (hold at its 45 % minimum power); with N facing, A was 61–63 Hz. Eddy model −0.07 to −0.10 (not subtracted). K: --. Camera video of the rig during these runs: `Video of 11_gap1.12_preregistered_flipped_and_measurement.mp4` (3 min 48 s, 1080p; re-encoded from the 320 MB original to 87 MB to fit GitHub's 100 MB file limit; the original is kept off-repo). |

**Raw logs are complete** (refreshed 2026-10-19): earlier copies of six sessions had been taken while the dashboard was still recording; they now hold the full `raw.csv` / `events.log`. The extra rows are all motor-off (idle after the runs). `eddy_table.json` = the eddy model's cached values (reproduces the eddy-model column). `chop_runs.json` / `run_flags.json` = every chop run and its flag. `chop_results.json` = latest chop result per label (the comparison reads this).

`calibration.json` holds the counts/g in use (100 = guess). `dashboard_settings.json` holds the cell rating, gap and max Hz.

## raw.csv columns (one row per 100 ms)

| Column | Meaning |
|---|---|
| `host_time` | PC clock, Unix seconds. Use this for time; the board clock restarts on reset. |
| `t_ms` | Board clock (ms since boot). |
| `force_raw` | HX711 average over the 100 ms, counts. **Scrambled readings exist while the motor runs:** stretches of exactly `-1`, about −152,000, or about +6,600 where the true level is about −304,000 to −309,000. Drop any value more than 20,000 counts from the motor-off level. |
| `f_Hz` | Rotor speed. **From folder 03 on: the coil FFT speed (real).** In folder 01 it's the old SS49E count (NOT real). 0 = no rotor line. |
| `motor_pct` | Motor power commanded to the board, 0–100 %. Each start from 0 also gets a 0.4 s kick (70 % in 01–02, 100 % from 03 on). This column doesn't show the kick. |
| `hall_mV`, `swing_mV` | SS49E on D0 (mean, swing). It does NOT see the rotor. It reads the motor's own field and rises with power. |
| `turns` | Folder 01: SS49E count (not real). From folder 03 on: turns integrated from the coil speed. |
| `coil_mV`, `coil_swing_mV` | Coil/INA333 on D2: mean and swing. |
| `hall_f_Hz` | (folders 03+) The SS49E's own speed guess, kept for comparison. It's wrong. |
| `coil_line_mV` | (folders 03+) Strength of the rotor line in the coil, about 0.05–0.09 mV per Hz when turning. |

The `02` coil captures have one row per window average, `hall_mV,coil_mV`, at **1953.1 rows/s**. Run an FFT and the rotor line is the single strong peak: 50 Hz at 50 %, 60 Hz at 60 %, 66 Hz at 70 %, 73 Hz at 80 %. The `near1` files were analysed assuming 1901 rows/s, so the speeds quoted from them read about 3 % low.

## Eddy currents (added 2026-10-15)

The spinning rotor magnet induces eddy currents in the **aluminium load-cell bar** (0.5 × 0.5 × 2.95 in) and in the **test magnet** (NdFeB conducts too). The currents push the cell **away from the rotor**, and the push **grows with speed** (opposite to the PFR, which falls as 1/f²). `tools/pfr/pfr_eddy.py` computes it for this geometry:
- **Rotor:** a dipole of 1.856 A·m² rotating about the spin axis.
- **Test magnet:** on the spin axis, flat on the bar face, 0.25 in from the free end.
- **Weighed part:** the bar from the free end to the gauge centre.
- **Conductivities:** 2024 aluminium σ = 1.74e7 S/m; NdFeB σ = 6.7e5 S/m.
- **Method:** 1.6 mm cubes with self-induction. Grid-converged to about 1%.

| Gap | 45 Hz | 65 Hz | 85 Hz |
|---|---|---|---|
| 0.40 in | 0.94 gf | 1.53 gf | 2.04 gf |
| 0.80 in | 0.32 gf | 0.51 gf | 0.67 gf |
| 1.12 in | 0.16 gf | 0.25 gf | 0.32 gf |

At 1.12 in that's about twice the PFR expected with K = 15. From 2026-10-15 on, the dashboard subtracts it:
- **Where:** saved points, E–H points, the sweeps and the chop. Columns `force_uncorrected_gf` / `eddy_gf` / `dP_uncorrected_gf` / `dP_eddy_gf` keep the raw values.
- **Test magnet removed:** only the bar part is subtracted.
- **Rotor magnet removed:** nothing is subtracted.

Unknowns that change it: the bar alloy (6061 is 44 % more conductive than 2024), and any other metal near the rotor (motor mount, screws). The **"test magnet removed"** control measures the bar's real eddy push directly, which checks the model.

## Things to know before trusting a number

1. **The motor-off reading has two levels about 400 counts apart** (at 2.5 in). Which one you get depends on where the rotor stops. A tare taken with the rotor stopped can therefore be off by ±200 counts. This caused most of the scatter in 01 and 04.
2. **Speed bands:**
   - The motor won't sit between about 65 and 85 Hz; it jumps across that band.
   - Above about 85 Hz (≥ 86–90 % power), the load cell reads mostly scrambled values.
   - Power for a given speed drifts as the motor heats (about 15 % weaker after an hour).
3. **No power above 90 % for more than 2 s.** User rule after 07: then 30 s capped. The dashboard enforces it now.
4. **Gap 2.5 in:** the model predicts about 0.0002–0.0008 gf there, far below what this rig can see. Folders 01, 04 and 05 all show a flat offset with no 1/f² trend. That's consistent with no measurable PFR at that gap.

## Key results so far

| Data | Result |
|---|---|
| Noise, motor off (01) | 25 counts per reading; SE of a 10 s mean 3 counts; drift −2 counts/min |
| Power → speed (02) | 40 %→40 Hz, 50→51, 60→61, 70→67, 80→72, 86→79, 90→95, 100→108 Hz (10 V supply, cool motor) |
| Speed hold (03, 04) | 45 / 50 / 54 Hz held within ±0.1–0.4 Hz |
| Gap 2.5 in (01, 04, 05) | No speed dependence within ~±40 counts; repeat spread ±129 counts (from the two-level tare) |
| Gap 0.8 in, ramp only (07) | Steady at +2,870 ± 30 counts over rest (rest = rotor at its stop angle), plus a small 1/f²-like part. Fit P = c0 + K·G1/ω² over 60–114 Hz (10 steps): K -- (withdrawn: post-hoc step selection; sign unverified). **Not conclusive:** one upward ramp, so speed and time rise together and drift looks the same as 1/f². The up-down cycles are what separate them. |

## Next data that will settle it

1. **B calibration** with masses: turns every number into real grams.
2. **The up-down sweeps at 0.8 in (or 0.4 in), 10 cycles.** Needed:
   - the mount repaired;
   - the motor cool;
   - max 90 % power;
   - ideally a cool-down between cycles.
   Sweep CSVs go in the new session folder: `sweeps_measurement_*.csv` + `_summary.txt`.
3. **A far-gap baseline sweep** (label "baseline"), so rig artifacts can be subtracted step by step.
