Releases: bartulem/usv-playpen
v0.10.6
v0.10.6 — CI fix: prettier drift; harden pre-push gate
Two non-runtime changes resolving the v0.10.5 CI failure and closing the
gap that let it slip past the local gate:
-
_metadata.yamlreformatted by prettier (single -> double quotes
onschema_versionand empty-string scalars). The drift was
introduced in commit 75b0ab2 but only surfaced when CI ran
prettier against--all-files; local pre-commit had no reason
to touch the unmodified file, so it sat inmainuntil the
v0.10.5 push finally tripped the CI Format job. -
.pre-commit-config.yamlnow installs a second pre-push hook
(pre-commit-format-check) that runspre-commit run --all-files --hook-stage manual— the exact invocation the CI
Format job uses. Drift in files nobody is currently editing is
rejected atgit pushtime rather than slipping past until a
red CI badge. Requires a one-time
pre-commit install --hook-type pre-pushon each clone. The
existing pre-push pytest hook is preserved; both fire on every
push. Bypass withgit push --no-verify.
No runtime, library, or output behavior changes vs v0.10.5.
v0.10.5
v0.10.5 — figure rendering fixes + urllib3 security bump
Five rendering bugs in make_neuronal_tuning_figures.py, all variations
of the same class (axis bound at one place, rendered data at another):
- Property tuning x-extent: filter by isfinite(rate) and shuffle
bands, not raw occupancy. Outlier bins with NaN rate from the
usv_property_min_occupancy_secondsgate no longer pull the
right axis past the rendered data. - Defensive
p0_5 = max(p0_5, 0)clamp on rendered shuffle floor
at all three render sites. - Categorical strip xlim: geometric (multiplicative) buffer when
symlog is in use; linear branch buffer bumped from 5% to 10% so
dots no longer sit on the spine. - Categorical strip: exactly two x-ticks at data extrema to stop
the symlog auto-ticker from overlapping labels. - Category PETH: curve and shuffle band extended to t=0 anchor so
the rendered data reaches the right axis bound.
Operational follow-ups surfaced during the audit:
analyses_settings.json:n_usv_min_self100 -> 30 to pass the
cohort-side vocal-compute gate on natural-variance sessions.generate_ratemaps_inference_global.sh: cluster resources
halved to 2 CPU / 16 G after 4 / 48 G was confirmed excess.
Security:
- urllib3 2.6.3 -> 2.7.0 (Dependabot alerts #1, #4, #38;
GHSA decompression-bomb safeguards bypassed in parts of the
streaming API, CWE-409).
Test coverage:
- 8 new tests in
tests/test_make_neuronal_tuning_figures.py
(~9 s): end-to-end PDF generation on both conventional and
scratch-cluster session layouts, full pkl-payload contract walk
(every top-level key + every nested block + shape consistency),
and one targeted regression per rendering fix.
v0.10.4
Hotfix release. Two regressions from the v0.10.2 cycle are corrected,
and a session-scoped pytest guard is added so the underlying pattern
cannot recur silently in CI.
Recording GUI restored on Windows recording PCs (commit 75b0ab2)
Symptom: after updating to v0.10.2 or v0.10.3, starting a recording from the GUI failed immediately with
Mount point /mnt/falkner/Bartul/Data on 10.241.1.205 is not valid.
Please fix the issue and try again.
even though the mount itself was healthy and the lab share was accessible via Explorer/SMB.
Root cause. The v0.10.2 paramiko hardening (commit 58ef3ba, RejectPolicy + load_system_host_keys()) silently required every recording-PC user account to have pre-populated %USERPROFILE%\.ssh\known_hosts via an interactive ssh user@host from the command line. On Windows recording PCs where the lab share is accessed via Explorer/SMB rather than via OpenSSH, that file never existed, so paramiko rejected the connection and check_remote_mount returned False for every user. check_camera_vitals then called sys.exit(1) immediately, tearing down the Qt process before the [**Remote mount check**] On 10.241.1.205, SSH connection error: … diagnostic line could be repainted in the GUI log widget. The user only ever saw the generic "Mount point … is not valid" line and had no information about why.
Fix.
ExperimentController.check_remote_mountreverts toparamiko.AutoAddPolicy()with an inline rationale comment and anlgtm[py/paramiko-missing-host-key-validation]suppression. The threat model — a one-shotos.path.ismount(...)probe across a closed lab subnet to the Motif PCs — does not justify the cost of breaking recording for every Windows account.- The ssh-rsa / SHA-1 mitigation from commit 6d400cd is retained. That one closes an actual upstream paramiko advisory (rsakey.py allowing SHA-1 on affected paramiko versions with no patched upstream release), not a CodeQL style finding. Modern OpenSSH on every Falkner-lab Motif PC negotiates
rsa-sha2-256/rsa-sha2-512without it. - Every
[**Remote mount check**] …line insidecheck_remote_mountis now emitted through a localemit()helper that calls bothself.message_output(...)andprint(..., flush=True). Future failures in this code path remain diagnosable from a terminal even when the GUI tears down before Qt can repaint the message widget.
Operational impact for upgrading users. None. AutoAddPolicy was the policy in v0.10.0 and v0.10.1, so the runtime behaviour returns to what it was before v0.10.2 shipped.
Bundled _metadata.yaml truncation — real root cause (commit 75b0ab2)
Symptom in v0.10.1 / v0.10.2. The packaged src/usv_playpen/_config/_metadata.yaml was zero bytes on disk after install, causing the recording GUI to crash on Record > Metadata with
QLineEdit(self.metadata_settings['Session']['institution'], …)
TypeError: 'NoneType' object is not subscriptable
v0.10.3 restored the file content but did not fix what was zeroing it.
Real root cause — and it was not commit ff8eef1. tests/test_record.py::test_conduct_recording_cli_invokes_controller_and_dumps_metadata (introduced in commit a87717a, the test-coverage expansion that took the suite from 25% to 52%) patched yaml.dump but not the open(metadata_path, 'w') that immediately precedes it inside conduct_recording_cli. Because open(path, 'w') truncates the file before yaml.dump is ever called, every single run of this test silently wrote zero bytes back to the tracked package file. Commit ff8eef1 only "committed an empty file" because somebody ran pytest shortly before staging — the truncation was happening on every test run before and after.
Fix. The test now wraps builtins.open with a selective side-effect: any write-mode open whose path ends in *_metadata.yaml is routed through unittest.mock.mock_open() instead of touching the filesystem, while read-mode opens (used by the CLI for the upstream behavioral_experiments_settings.toml and the initial _metadata.yaml load) pass through to the real handler unchanged.
Src-tree integrity guard (commit 87fbc04)
The pattern that caused the empty-YAML disaster — a test mutating a tracked package file as a side effect — was undetectable in CI because no assertion ever checked. The v0.10.4 release adds a session-scoped guard so the entire class of bug is caught immediately:
tests/conftest.pynow definespytest_sessionstart, which walkssrc/usv_playpenand stores a{path: sha256}baseline onsession.config._src_tree_baseline. Generated/cache artifacts that legitimately fluctuate during a session are excluded:__pycache__/,.pytest_cache/,.mypy_cache/,_version.py(hatch-vcs generated),.DS_Store,*.pyc,*.pyo.tests/test_src_integrity.py::test_src_tree_unmodified_during_test_sessionre-hashes the same tree at session end and fails with a per-fileMUTATED:/ADDED:/REMOVED:report if anything changed. The error message points directly at the offending paths, so the offending test is easy to bisect.pytest_collection_modifyitemsinconftest.pyre-orders the collected items sotest_src_integrityalways runs last, after every other test in the session — that way the integrity check observes the cumulative effect, including writes that legitimately happen during GUI startup (sync_equipment_dynamic_fieldsre-saving_metadata.yaml) and unintended writes from buggy mocks.
With this in place, a future test that forgets to mock a write fails the session with output like
Files under src/usv_playpen were modified during the pytest session.
MUTATED:
- _config/_metadata.yaml
instead of silently shipping the corrupted file in the next tag.
Test-suite changes
test_check_remote_mount_success: assertsAutoAddPolicy(notRejectPolicy); the ssh-rsa SHA-1 mitigation assertion is unchanged.test_check_remote_mount_handles_missing_known_hosts: removed; obsolete now thatAutoAddPolicyis restored.test_conduct_recording_cli_invokes_controller_and_dumps_metadata: patchesbuiltins.openfor*_metadata.yamlwrites.test_src_tree_unmodified_during_test_session: new — described above.
Full suite (567 tests, excluding the inference and anipose suites that require GPU and Aniposelib setup) passes locally; _config/_metadata.yaml is unchanged after a complete session.
Upgrade notes
- No schema or behavioural changes outside the recording GUI's SSH probe. Pull v0.10.4 on every recording PC before the next session.
- No migration is required for existing per-session metadata YAMLs.
- If you have an installation of v0.10.1 / v0.10.2 / v0.10.3 in flight and the bundled
src/usv_playpen/_config/_metadata.yamllooks empty, this release will replace it with the canonical 81-line template; no manual recovery is needed.
v0.10.3
Recording GUI hotfix
-
Restored bundled
_config/_metadata.yaml(355350a): the file was accidentally truncated to 0 bytes in commit ff8eef1 (shipped in v0.10.1 and v0.10.2), which caused the recording GUI to crash the moment the user reached Record > Metadata:self.institution_edit = QLineEdit(self.metadata_settings['Session']['institution'], …) TypeError: 'NoneType' object is not subscriptableThe crash had a clean root cause —
yaml.safe_loadon an empty file returnsNone, the defensive guard insidesync_equipment_dynamic_fieldssilently absorbed the non-dict, and the failure surfaced only whenrecord_threestarted subscriptingmetadata_settings['Session'][...]. The 81-line canonical template (Session / Environment / Equipment / Subjects blocks) has been restored verbatim from fdabdf2.
Tests
tests/test_gui.py::test_bundled_metadata_yaml_is_a_dict(355350a): parses the on-disk_config/_metadata.yamland asserts it loads as adictwithSession/Equipmentblocks and every Session key thatrecord_threereads (institution,lab,session_experiment_code,calibration_session,session_usv_playback_file,session_description,keywords,notes). Fails instantly on truncation or corruption of the shipped template.tests/test_gui.py::test_navigation_to_record_metadata_window(355350a): drivesRecord → Next → Nextend-to-end to actually open the Metadata window inpytest-qt, then assertsapp.institution_edit.text() == app.metadata_settings['Session']['institution'](andlab_editlikewise). The previous GUI test suite only ever reachedrecord_one, which is why the empty-YAML regression slipped past CI. Had this test existed before ff8eef1, the crash would have been caught in CI rather than on the recording PC.
Upgrade notes
- No behavioral or schema changes — pure regression fix. Anyone already running v0.10.1 / v0.10.2 should pull v0.10.3 before the next recording session; no migration of existing per-session metadata YAMLs is required.
v0.10.2
Security
- paramiko host-key validation (58ef3ba):
ExperimentController.check_remote_mountnow usesRejectPolicy+load_system_host_keys()instead ofAutoAddPolicy. Closes the CodeQLpy/paramiko-missing-host-key-validationfinding and removes a man-in-the-middle attack surface on lab-internal SSH probes to Motif PCs. Operationally invisible — every lab user has already populated~/.ssh/known_hostsfor these hosts. - Legacy ssh-rsa (SHA-1) disabled (6d400cd):
disabled_algorithms={"pubkeys": ["ssh-rsa"]}passed toparamiko.SSHClient.connect, dropping the deprecated SHA-1 host-key algorithm in favor ofrsa-sha2-256/rsa-sha2-512(supported by all current Motif PC OpenSSH builds).
Behavioral experiments
- Both paramiko changes above touch
src/usv_playpen/behavioral_experiments.py(ExperimentController.check_remote_mount). Behavior is unchanged for any session whose host key is already trusted; sessions against unknown hosts now fail loudly during the SSH probe instead of silently auto-trusting.
Tuning curves
- Session duration now derived from the tracking H5 in
compute_neuronal_tuning_curves._load_vocal_inputs—tracks.shape[0] / recording_frame_rateinstead ofaudio_triggerbox_sync_info.json. Anchors the shuffle wrap and bout bookkeeping to the same camera/behavioral time base the spike data is aligned to. The audio sync JSON is no longer required bygenerate-rm, simplifying cluster/remote transfers.
v0.10.1
Highlights
This release adds per-cluster neuronal tuning curves and a threshold-based triage layer for surfacing biologically interesting neurons, plus a fix for a multi-user Windows mount failure that has been silently affecting recordings on shared lab PCs. Test coverage doubles again, from 25% to 52%.
Neuronal tuning curves (replaces behavioral tuning curves)
compute_behavioral_tuning_curves and make_behavioral_tuning_figures are gone. The replacement is a single NeuronalTuning class (extending FeatureZoo) with one entry point and one output pkl per cluster — both behavioral and vocal tuning live in the same per-cluster file:
ephys/tuning_curves/{cluster_id}_tuning_curves_data.pkl
Each side is internally gated on the presence of its own inputs. Missing inputs produce a logged skip, not a crash. Running the two sides out of order, or only one of them, never clobbers the other's output.
Behavioral path
Driven by *_behavioral_features.csv + the translated/rotated/metric tracking H5. For every cluster the pipeline computes:
- Per-temporal-offset 1D feature ratemaps, with shuffled-spike nulls. Offsets are configurable; the default sweep spans the relevant pre/post windows.
- Per-animal 2D spatial ratemap (occupancy-normalised firing rate over the arena floor).
- A
circular_featuressetting tells the smoother which features wrap (allo_yaw,body_dir, etc.) so Gaussian smoothing crosses the boundary correctly.
Smoothing is controlled by a single smoothing_sd parameter (in bins). 1D smoothing routes through a circular-aware Gaussian; 2D smoothing uses standard scipy.ndimage.
Vocal path
Driven by *_usv_summary.csv + the tracking H5 + the audio sync metadata. Four orthogonal vocal payloads are computed per cluster:
| Payload | What it is |
|---|---|
usv_peth |
Pooled pre-USV peri-event time histogram per emitter side. Default window [-2, 0] s, 50 ms bins. |
usv_property_tuning |
Within-USV firing rate vs each continuous acoustic property: duration, mean / peak frequency, bandwidth, mean / max amplitude, spectral entropy, mask number. |
usv_category_tuning |
Per-category within-USV firing rate for the four categorical features: vae_category, vae_supercategory, qlvm_category, qlvm_supercategory. |
usv_category_peth |
Per-category time-resolved peri-USV PETH grid. |
Each payload is computed per emitter (self / partner / unassigned) and each emitter carries an explicit role annotation, so downstream code never has to re-derive who emitted a given USV.
The include_partner_vocalization_tuning flag (default True) controls whether partner-emitted USVs participate in the vocal payloads, giving you a way to compute "self only" tuning if you suspect the partner-emitter assignments are unreliable for a given session.
Triage statistics block
Every cluster pkl carries a triage_stats block written during compute. This block is the input to the new triage layer (see below) and is intentionally exhaustive so that re-running triage with new thresholds never has to touch spike or USV data. The full per-cluster set:
- VMI per emitter (Vocalization Modulation Index, per Mimica et al.) — paired (in-bout vs out-of-bout) firing rates, the resulting VMI scalar, the bout count, and a Wilcoxon signed-rank two-sided p-value per emitter.
- Bouts — sliding-gap detection of contiguous USV groupings used by VMI.
- Run analysis on every 1D modality (behavioral feature,
usv_peth,usv_property_tuning,usv_category_peth) — for both excitatory and suppressive directions: the longest consecutive-bin run above the per-direction z-threshold, thepeak_zmagnitude, and axis annotations (run_t_start/run_t_end/peak_t/range_low/range_high). - Selectivity index (Vinje–Gallant).
- Monotonicity via Spearman ρ over bin centres.
- Skaggs information rate (bits/spike) and Skaggs sparsity for spatial maps.
- Spatial coherence (smoothed-vs-raw correlation).
- Ramp index for time-resolved PETHs.
- Categorical peak-abs-z and best category index for the four categorical features (no run analysis — categories are unordered).
Two long-standing math issues were resolved while wiring this up:
- The Skaggs information rate previously filtered to
rate > 0before normalising the mean rateR, biasing the estimate downward for sparsely-firing units. The fix computesRover all valid-occupancy bins, then sums only over bins withr > 0— matching the original Skaggs et al. formulation. - The categorical strip x-scale auto-detector (
_decide_strip_xscale) considered zero values when computing dynamic range, which collapsed the ratio to NaN/inf for any category with a zero bin. The fix anchorsminon strictly-positive values only and returnslineargracefully when the input is all-NaN or all-non-positive.
Detect-interesting triage layer
detect_interesting_tuning_neurons.detect_interesting_clusters is a pure pkl→JSON pass. It scans every *_tuning_curves_data.pkl under ephys/tuning_curves/, applies the configured thresholds to each cluster's triage_stats block, and writes:
ephys/tuning_curves/interesting_neurons_<YYYYMMDD>_<HHMMSS>.json
with two views of the same flagged set: by_modality (which clusters fired which flag) and by_cluster (per-cluster details, evidence, and emitter role).
Four flag families:
| Flag | Gate | Direction |
|---|---|---|
vmi_<role>_excit / vmi_<role>_suppress |
wilcoxon_pvalue < α AND n_bouts ≥ min_bouts AND vmi finite |
Sign of VMI |
<modality>_<role>_excit / <modality>_<role>_suppress (1D run-analysis modalities) |
max_run ≥ min_consecutive_bins AND ` |
peak_z |
usv_category_<role>_<feat> |
peak_abs_z ≥ z_threshold |
Single (categories are unordered, no direction) |
spatial_<role> |
info_rate_bps ≥ spatial_info_bps_threshold |
Single (Skaggs gate; sparsity & coherence reported but not gated) |
All thresholds live under analyses_settings.json → detect_interesting_tuning_neurons so they can be retuned and the JSON regenerated without re-running compute. The output JSON is timestamped, never overwritten, so threshold sweeps accumulate as separate files.
CLI entry point: detect-interesting (see usv-playpen detect-interesting --help).
Per-cluster figure renderer
make_neuronal_tuning_figures.NeuronalTuningFigureMaker produces one combined output file per cluster. Output is configurable via neuronal_tuning_figures.fig_format:
pdf(default) — single multi-page file per cluster.png/svg/eps— one file per page, suffixed_p{N}_{label}.
Page layout is fixed:
- Behavioral pages — one page per temporal offset, per plot-feature group. Per-mouse colour palettes from
extract_information(experiment_code). - Vocal page 1 — left half: bout raster + pooled
usv_pethoverlay; right half: 4×4 grid of within-USV firing rate vs continuous acoustic property. - Vocal page 2 —
usv_category_tuningwatershed plots over the bundled UMAP segmentation (_config/vocal_umap_segmentation.npz), plus the per-category time-resolvedusv_category_pethgrid.
A new ratemap_cmap setting controls the colormap used for the 2D spatial map; neuronal_fig_format controls the output container. Both are exposed in the GUI's Visualize tab.
GUI updates
The Analyze tab now exposes:
include_partner_vocalization_tuning_cbbehavioral_min_occupancy_seconds,usv_property_min_occupancy_secondssmoothing_sd- A
detect_interesting_tuning_neuronsblock with all five thresholds visible side-by-side
The Visualize tab adds dropdowns for:
neuronal_fig_format_cb(pdf / png / svg / eps)ratemap_cmap_cb
Smoke tests covering Record / Process / Analyze / Visualize tab construction are part of the new test suite, so future schema renames that misalign GUI dict-paths are caught at CI time rather than at first user click.
Bugfix — stale \\cup credential on shared lab Windows accounts
This is a real bug that fired during a recording session. After Ethernet reconnection, `net use F: \\cup\falkner ... /user:bob@princeton.edu /persistent:yes` returned System Error 1326 ("Logon failure: unknown user name or bad password") even though the credentials were correct. The next session worked unchanged.
Root cause: when the previous session mounted with `/persistent:yes`, Windows stored a `Domain:target=cup` entry in Credential Manager, keyed on whoever ran that session. On a Windows account shared by multiple lab members, the next mount silently routes through the previous user's stored credential and fails authentication against the KDC.
`klist purge` and `net use \\cup /delete` do not touch the Credential Manager cache. Three cleanup mechanisms exist, and we were only running two:
| Cache | Cleared by | Was clearing? |
|---|---|---|
| Active SMB session | `net use ... /delete` | yes |
| Kerberos ticket cache | `klist purge` | yes |
| Credential Manager / Vault | `cmdkey /delete:cup` | no |
Fix: `purge_cup_connections_on_windows` now runs `cmdkey /delete:cup` alongside `klist purge`. All three caches are cleared on every remount. `/persistent:yes` stays — the workflow that "F: drive is still mounted between sessions for browsing" continues to work.
Other fixes
- `FindMouseVocalizations.init` had a dead default-loading branch that referenced a non-existent `_defaults["usv_inference"]["root_directory"]` JSON key — would crash with `KeyError` if anyone ever constructed the class with all defaults. Branch removed.
- `summarize_das_findings` parsed annotation filenames with `name.split("")[0] + "" + name.split("_")[2]`, which broke on multi-segment timestamps (e.g. `m_20260101_120000_ch01_annotations.csv`). Replaced with an anchored regex; mismatches log + skip ra...
v0.10.0
Highlights
This release is the largest single-version jump in the project's history, focused on test coverage, codebase clarity, and CI hygiene.
Tests
- Test suite expanded from 32 → 238 tests (
tests/), all passing in ~75 s on the local dev machine. - Two production bugs caught and fixed in the process:
modify_files.concatenate_binary_fileswas opening a file inside a directory before that directory was created — first runs on a new probe day would have failed with an opaqueFileNotFoundError. Mkdir is now hoisted before the write.compute_behavioral_features.{calculate_tail_curvature, get_euler_ang}usednp.reshape(..., newshape=...), deprecated in NumPy 2.1; would have started failing pytest'sfilterwarnings=["error"]config. Both call sites now use the positionalshape=form.
- Eight
next(...)/glob()[0]lookups inassign_vocalizations.pyandmake_behavioral_videos.py, plus threenext(key for key in dict)lookups insynchronize_files.py, replaced withfirst_match_or_raise(...)(or labeledKeyError). Missing-input failures now surface withFileNotFoundError: <label>: no match for glob '<pattern>' under '<root>'instead of context-freeStopIteration/TypeError: NoneType.
Identifier renames (clarity)
ivi is gone from the codebase — both as a substring of identifiers and as a standalone abbreviation. The replacement names are explicit:
| old | new |
|---|---|
IVICalculator |
InterUSVIntervalCalculator |
compute_ivi_distributions[.py] |
compute_inter_usv_interval_distributions[.py] |
compute_ivi_distributions_bool |
compute_inter_usv_interval_distributions_bool |
ivi_archive[.py] |
usv_interval_archive[.py] |
ivi_summary_statistics[.py] |
usv_interval_summary_statistics[.py] |
usv_ivi_gmm_plots.ipynb |
usv_interval_mixture_models_plots.ipynb |
generate-ivi-gmm (CLI) |
generate-usv-interval-distributions |
IVI / IVIs (acronym) |
inter-USV interval / inter-USV intervals |
The notebook was renamed gmm → mixture_models because the underlying gmm_utils.py already houses both GaussianMixture (sklearn) and a Student-t TMixture, so "GMM" was a misleading label.
275 occurrences updated across 12 files; 4 file renames; 30 explanatory markdown cells added to the two analyses_notebooks/*.ipynb while at it.
Codebase hygiene
- Stripped 2,027 dashed-banner lines (
# ----,----------section dividers in docstrings, etc.) fromsrc/anddocs/. Pure-dashed lines deleted; banner-wrapped comments had dashes removed and inner text kept. marimomoved from runtimedependenciesto[dependency-groups].devsince runtime code does not import it.
CI
- Workflow renamed
CI→testsso the README badge reads "tests". - Python version pinned to 3.13 (was 3.10 — below the project's
requires-python = ">=3.11,<3.14", so all recent runs were silently failing atuv sync). - Matrix narrowed to Linux + macOS; Windows dropped because recording-side dependencies (Avisoft, motifapi, imgstore) lack Windows wheels for non-recording use.
- Codecov upload step set to
continue-on-errorso a missing token cannot turn the badge red. - Two new badges in
README.md: tests-status (from this workflow) and Codecov coverage.
Visualization fixes
- LRT bootstrap broken-axis plots: matched diagonal break markers now render at the same visual angle on both halves regardless of GridSpec width ratios (previously the slashes were drawn as line-segments in axes coordinates, so each half's slope was
h/wand they looked subtly mismatched). Switched to a marker-based approach that's pixel-sized. - Same plots: removed the spurious top-of-subplot break markers and unblocked the lower x-axis limit (was clamped to 0, preventing legitimate negative
lr_nullranges from rendering naturally).
Full Changelog: v0.9.17...v0.10.0
usv-playpen v0.9.17
TL;DR
Reverts the v0.9.15 orientation-gate rewrite that left the SEI distribution heavily skewed toward zero. v0.9.16 (bounded gamma) recovered the high tail but the bulk
stayed near zero (median ~0.014 on the test session, vs. ~0.05 expected from scored video). Restoring the v0.9.14 form — 3D cosine similarity in a single scalar,
unbounded gamma, signed output — gives the empirical distribution we previously verified.
What changed
calculate_sei in analyses/compute_behavioral_features.py:
v0.9.17 (this release) — restored from v0.9.14
v_h = obs_nose - obs_head # 3-D gaze direction
v_t = target_point - obs_head # 3-D target direction
cos_theta = dot(v_h_unit, v_t_unit) # ONE scalar, full 3-D angle
gamma = 1 + 1 / (d_norm + 1e-6) # unbounded
sei = sign(cos_theta) * |cos_theta|**gamma * w_social # signed, [-1, 1]
Replaces v0.9.15+'s separable Gaussian exp(-yaw²/(2σ²)) * exp(-pitch²/(2σ²)) and the v0.9.16 bounded gamma = 1 + tanh(1/d_norm).
The Gaussian gate decays from the start and has no plateau near θ=0, so even moderately aligned engagement frames at small (yaw, pitch) errors got penalised.
Compounding pitch as a separate multiplicative factor double-charged the same alignment error already in yaw. Combined with the bounded gamma from v0.9.16 the high tail
recovered but the bulk of the distribution still sat at zero — median SEI ~0.014.
cos plateaus near 1.0 around the 0° gaze axis, so on-axis frames survive even very large exponents (cos(10°)**21 = 0.72). The signed output keeps partner-behind frames
negative rather than collapsing them to a small-positive smudge near the median.
Surface-level changes
- Output range: [0, 1] → [-1, 1] (signed). Downstream consumers that want a strictly non-negative score can clip with np.clip(sei, 0.0, 1.0).
- Function signature: unchanged. observer_head_root, sigma_yaw_deg, sigma_pitch_deg are accepted but no longer consumed (kept so the call sites in the extraction loop
don't have to change). - Standalone (yaw, pitch) feature columns added in v0.9.15 (.allo_yaw-, .allo_pitch-) are unaffected; only the SEI gate reverts.
- Histogram bounds in FeatureZoo.feature_boundaries widened to match the new dynamic range:
- orofacial-sei / anogenital-sei: [0, 1] → [-1, 1]
- *-sei_1st_der: [-6, 6] → [-12, 12]
- *-sei_2nd_der: [-36, 36] → [-72, 72]
Required follow-up
Re-run feature extraction on every cohort whose orofacial-sei / anogenital-sei distribution you care about. Cached
*_points3d_translated_rotated_metric_behavioral_features.csv files generated under v0.9.15 (Gaussian gate + unbounded gamma → crushed) or v0.9.16 (Gaussian gate +
bounded gamma → flat-near-zero) will need regeneration before any aggregation or modeling input pickle that depends on the SEI features is rebuilt.
The four downstream modeling pipelines that consume dyadic-engagement features (onsets, binomial category, multinomial, manifold) will need their input pickles rebuilt;
bout-params does not depend on engagement features and is unaffected.
Commits
- ca92316 — calculate_sei: revert orientation gate to 3D cosine + signed output
History
- v0.9.14 (8ccc87b) — known-good 3-D cosine SEI; this release restores its math.
- v0.9.15 (83e7435) — introduced separable Gaussian gate over (yaw, pitch); broke the SEI distribution.
- v0.9.16 (6a1f55d) — bounded gamma to [1, 2]; partial fix, still flat near zero.
- v0.9.17 (this release) — full revert of the SEI gate to v0.9.14 math.
usv-playpen v0.9.15
- New get_egocentric_direction function (3D R_A · v decomposition into signed yaw and pitch in the observer's anatomical head frame)
- Old calculate_planar_social_angle removed
- Legacy yaw quartet rewritten to be 3D-egocentric (corrects the planar-projection bias under high pitch)
- New pitch quartet added (allo_pitch-nose, nose-allo_pitch, allo_pitch-TTI, TTI-allo_pitch) with 1st/2nd derivatives
- speed_diff and neck_elevation_diff (and their derivatives) dropped
- SEI rewritten as a separable Gaussian gate over (yaw_ego, pitch_ego), output now in [0, 1] with 0 = not attending, 1 = attending
(with tunable σ_yaw and σ_pitch defaulting to 45°) - feature_boundaries and feature_labels updated; multi-session audit confirms 0 features clip ≥ 2% across 121 sessions / ~43M samples
per feature - File-level docstring [B] block rewritten to reflect the new social feature layout
usv-playpen v0.9.14
v0.9.14: behavioral feature math fixes — anatomical head root, reproducible back angles, corrected tail curvature, audited histogram bounds