Repository navigation
Named simulation profiles: the fdi_semg card - #42
Merged
Merged
Conversation
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
force-pushed
the
feat/named-profiles
branch
from
November 7, 2026 11:09
a68ddbc to
e3a41d3
Compare
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.
# Conflicts: # docs/literature.md
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fifth of five stacked PRs: #40 → #43 → #44 → #45 → this. Base is
feat/fdi-fibre-typing(#45); retarget tomainonce 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.fdi_semg(default): the validated FDI surface-EMG settings (120 units, the calibrated descending drive from FDI descending-drive calibration tooling #38, the typed fibre model (fiber_model: typed, opt-in since Opt-in fibre types by motor unit, surface EMG from Muscle fibres, FDI fibre diameter #45), fibre dispersion and λ 0.63 fromMusclefibre model: typed by motor unit, Polgar diameters, Del Vecchio CVs, within-unit spread #35–Rosenfalck IAP length scale option and the FDI fit #37).fdi_semg.published: each component as its source paper describes it (Fuglevand 1993 force–frequency, Rosenfalck λ = 1, no fibre dispersion), with its own MVC mapping.card.thresholds(),card.muscle(),card.force_model(),card.motor_neuron_pool(),card.surface_emg(muscle, electrodes), andcard.simulate([(duration_s, start_pct_mvc, end_pct_mvc), ...]), which runs the calibrated drive in % MVC and returns spike trains and force.ProfileNotewarnings). Every override, the build's seed and passed-through keywords are recorded (profile_record,myogen_profileblock annotation).pyyamlis a declared dependency.build_pools,run_descending_drive); the optimiser imports it. It wires the drive withNetwork.connect_fan_infrom DD optimiser, force-frequency option, Network.connect_fan_in, literature register #44.CHANGELOG.md).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 inmyogen/profiles/_builders.pyandexamples/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.pyruns 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 ofbenchmarks/semg_dispersion). Rerun on the rebased code (a6fabd87d), same scores to floating-point precision:fdi_semgpasses (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 fromMYOGEN_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.