Skip to content

Named simulation profiles: the fdi_semg card - #42

Merged
DRohlf merged 24 commits into
mainfrom
feat/named-profiles
Nov 7, 2026
Merged

DRohlf merged 24 commits into
mainfrom
feat/named-profiles

Conversation

@DRohlf

@DRohlf DRohlf commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Fifth of five stacked PRs: #40 → #43 → #44 → #45 → this. Base is feat/fdi-fibre-typing (#45); retarget to main once those merge.

What it adds. myogen.load_profile() loads a profile card: every validated setting of one simulation in a YAML file, each value with its source (a literature-register key with a locator, or "assumption"/"derived"), and, where it matters, a note on what else to check when it is changed.

Seeding: a deliberate deviation. In MyoGen, library code never seeds; the script owns the seed. Card builders are the exception: each build draws from the card's seed for that stage (or seed=), so it reproduces the validated runs regardless of call order, and then gives the caller's generator and seed back (myogen._seed_scope), without printing. Documented in myogen/profiles/_builders.py and examples/01_basic/README.md, with what reproduces: on any platform the validated thresholds and fibres exactly, the same spike counts, and spike times to NEURON's floating point (about 1e-4 relative between platforms).

Validation. benchmarks/profiles/validate_card.py runs the card end to end at 25 % MVC on the Delsys 5-pin setup and scores it against limits frozen before the script existed (±10 % RMS, ±5 % median frequency around arm F of benchmarks/semg_dispersion). Rerun on the rebased code (a6fabd87d), same scores to floating-point precision: fdi_semg passes (cc 0.200 mV / 161 Hz, adj 0.234 mV / 156 Hz, hold 25.6 % MVC; Botelho et al. 2019: 0.21 mV, ~165 Hz). The published variant is recorded, not judged (cc 0.247 mV / 228 Hz). The validation samples at 10240 Hz only to score the comparator's 20–2000 Hz band; generated sEMG stays at 2048 Hz.

Tests. Full suite at 376bc7a7d: 618 passed, 3 skipped. The third skip is the arm-F scorer test, which reads its 33 MB input from MYOGEN_ARM_F_NPZ. The descending-drive and card-fibre tests compare stored values instead of SHA-1 hashes (the spike-time sum to 1e-3, for NEURON across platforms); the old hashes still match on the rebased code. Examples 01, 03, 04, 05, 06 and 08 ran end to end before the rebase, which did not change them.

DRohlf added 18 commits November 7, 2026 10:30
The DD pool, equal fan-in wiring with dendritic-transfer weights, scaled Gfluctdv and the
drive loop move from examples/02_finetune/06_optimize_dd_multilevel.py into
myogen.simulator.neuron.descending_drive_network; the optimiser imports them (one
authority, shared with the coming profiles). Spike trains are unchanged: a 1.5 s run of the
FDI calibration (Gfluctdv off) gives the hash pinned from the optimiser before the move.
load_profile() reads YAML cards from myogen/profiles: every validated FDI surface-EMG
setting with its source, unit, note and calibration flag, plus the fdi_semg.published
variant (parent + changed fields only). The calibration values (trial 85 of _human59_wexp,
the MVC curve and per-unit recruitment forces) are copied into the card with their
provenance. Register rebuilt for the moved drive citation.
Card notes are ProfileNote warnings shown once per key per process; Muscle, SurfaceEMG,
ForceModel and AlphaMN__Pool built by hand print a one-line hint pointing to
load_profile('fdi_semg'), once per run and never inside a card build.
Profile.thresholds / muscle_kwargs / muscle / force_model / dd_params / motor_neuron_pool /
surface_emg_kwargs / surface_emg build the library objects from the card, each reseeded
from the card's seeds. A keyword equal to the card value (unit-aware) is not an override;
a differing one is recorded (overrides(), record()) and prints the field's note once per
run. Built objects carry profile_record; SurfaceEMG annotates its result block with it.
Profile.excitation_for_pct_mvc interpolates the card's MVC-ramp curve (0-100 % MVC, raises
outside); Profile.simulate runs the calibrated drive for (duration_s, start_pct_mvc,
end_pct_mvc) segments and returns spike trains (annotated with the card record), force in
% MVC and the excitation. A card without an MVC mapping refuses % MVC levels.
Examples 01, 03, 04, 05, 06 and 08 build their components with load_profile(): card
thresholds, the calibrated drive in % MVC (03 replaces its Poisson loop; 08 uses the 03
spike trains instead of current injection), card.muscle(), card.surface_emg() with the
script's own electrodes, card.force_model(). README section on profiles; CHANGELOG;
backlog item 1 done.
benchmarks/profiles: delsys.py (the cc/adj scorer of the frozen limits; reproduces arm F)
and validate_card.py (card end to end at 25 % MVC on eight Delsys 5-pin sensors, or the
MVC mapping). fdi_semg passes: cc 0.200 mV / 161.2 Hz, adj 0.234 mV / 156.2 Hz. The
published variant gets its own MVC mapping (13471 model units) and a recorded, unjudged
run (cc 0.247 mV / 227.5 Hz). Both cards carry a validation block. Register rebuilt for
shifted line numbers.
The results/ ignore rule caught them; the cards' validation blocks point to these files.
Overrides now apply where they are recorded (simulate, motor_neuron_pool), carry across
builders of one card instance (passing the card value clears one), are converted to the
card unit, and equal dataclass / unit-CV values are not overrides. Pass-through keywords
are listed under keywords in profile_record. Cards missing a required field or seed fail
at load. The excitation curve extends to 0 / 100 % MVC. Card-built SurfaceEMG draws its
MUAPs from the build-time generator state. Internal sampling stays the library default
(validate_card passes 10240 Hz explicitly, as arm F). Sources without a read locator are
labelled assumption; the variant's shape note is corrected.
- calibrated fields without a note still print a calibration note
- notes and the hand-built hint point at the caller's line, not MyoGen internals
- notes are keyed by card and field, so the same field on two cards notes twice
- record() carries the parent card's version
- tests: card-built SurfaceEMG and pools give no hand-built hint
- example 04: the thresholds are regenerated from the card seed
…rator back


Card builders drew from set_random_seed(card seed), which replaced the user's
generator and seed, reset what derive_subseed returns and printed on every
build; simulate_muaps likewise overwrote the user's generator state.

Builds now draw inside myogen._seed_scope: same draws as before (fingerprint of
thresholds, pool wiring, spike-generator sub-seeds and card MUAPs unchanged),
then the caller's generator and seed are restored, without printing. Each
drawing builder takes seed= (default: the card's), recorded as profile_record
seed. Documented as a deviation from base MyoGen, where library code never
seeds and the script owns the seed.
Record the decisions made during implementation: scoped seeding (a
deviation from base MyoGen), cumulative overrides, notes per card and field,
recorded pass-through keywords, library-default internal sampling rate.
fdi_semg passes (cc 0.2004 mV / 161.3 Hz, adj 0.2340 mV / 156.3 Hz, hold
25.6 % MVC); the published variant is recorded (cc 0.247 mV / 227.5 Hz).
…ace SHA-1 pins


Fibre types by motor unit are now opt-in (Muscle(fiber_model="typed")), and the
cards were validated with them, so fdi_semg sets fiber_model: typed (the published
variant inherits it). The arm-F fibre test and the descending-drive spike test
compared SHA-1 hashes of raw floats, which differ across platforms; they now
compare stored values with a relative tolerance. Both old hashes still match on
this rebased code, so the validated fibres and spikes are unchanged.

The card's provenance points at repository paths: the optimiser in
examples/02_finetune and the per-unit CV inputs vendored in
benchmarks/semg_dispersion/unit_cv.
The MVC-ramp script is examples/02_finetune/07_fdi_spikes_force.py. The arm-F
scorer test reads its 33 MB input from MYOGEN_ARM_F_NPZ instead of a local
folder, and skips without it.
On a6fabd8 both cards give the recorded scores to floating-point precision
(fdi_semg cc 0.2004 mV / 161.25 Hz, adj 0.2340 mV / 156.25 Hz, inside the frozen
limits; published cc 0.2470 mV / 227.5 Hz). The MVC mapping is unchanged (same
drive and force model) and was not rerun.
@DRohlf
DRohlf force-pushed the feat/named-profiles branch from a68ddbc to e3a41d3 Compare November 7, 2026 11:09
@DRohlf
DRohlf changed the base branch from calib/fdi-dd-main to feat/fdi-fibre-typing November 7, 2026 11:09
- _seed_scope catches ImportError only when resetting the cell ID counters,
  and gives the caller's generator back if the reset raises.
- pyyaml is a declared dependency: import myogen loads the cards (uv.lock
  updated).
- The network-block error names MyoGen 0.13.0 instead of a date.
- The spike test compares the sum of spike times to 1e-3: NEURON's floating
  point differs between platforms (macOS arm64: counts and first spikes match,
  sum 1.1e-4 higher).
- The card docs say what reproduces on any platform: thresholds and fibres
  exactly, spike counts, and spike times to NEURON's floating point.
… on the cards

Muscle, SurfaceEMG, ForceModel and AlphaMN__Pool built by hand no longer print
a hint pointing to the fdi_semg card: the card is FDI-specific, and the hint
reached users simulating any muscle. The card-build guard it needed
(building_from_card) goes with it.

The cards cite Fuglevand et al. 1993 with locators: exponential thresholds
p. 2472 eq. 1, force-frequency saturation S = 1 - exp(-2 r^3) p. 2475 eq. 16.
@RaulSimpetru
RaulSimpetru changed the base branch from feat/fdi-fibre-typing to main November 7, 2026 18:04
On Linux x86_64 unit 8 fires once more than stored (22 against 21, at
1.496 s of a 1.5 s run). NEURON's floating point differs between
platforms: once the network diverges, spike times shift by whole time
steps, and a spike near the end of the run can come or go. macOS arm64
and Windows match every count.

Counts are now compared to one spike per unit. First spikes stay at 1e-9
(exact on Linux and macOS) and the sum of spike times at 1e-3 (Linux
5.9e-4, macOS 1.1e-4). The card docs no longer promise the same spike
counts on every platform.
@DRohlf
DRohlf merged commit 46294a0 into main Nov 7, 2026
14 checks passed
@DRohlf
DRohlf deleted the feat/named-profiles branch November 7, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants