Skip to content

Releases: bartulem/usv-playpen

v0.10.6

11 May 19:10

Choose a tag to compare

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.yaml reformatted by prettier (single -> double quotes
    on schema_version and 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 in main until the
    v0.10.5 push finally tripped the CI Format job.

  • .pre-commit-config.yaml now installs a second pre-push hook
    (pre-commit-format-check) that runs pre-commit run --all-files --hook-stage manual — the exact invocation the CI
    Format job uses. Drift in files nobody is currently editing is
    rejected at git push time rather than slipping past until a
    red CI badge. Requires a one-time
    pre-commit install --hook-type pre-push on each clone. The
    existing pre-push pytest hook is preserved; both fire on every
    push. Bypass with git push --no-verify.

No runtime, library, or output behavior changes vs v0.10.5.

v0.10.5

11 May 18:55

Choose a tag to compare

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_seconds gate 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_self 100 -> 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

11 May 14:54

Choose a tag to compare

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_mount reverts to paramiko.AutoAddPolicy() with an inline rationale comment and an lgtm[py/paramiko-missing-host-key-validation] suppression. The threat model — a one-shot os.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-512 without it.
  • Every [**Remote mount check**] … line inside check_remote_mount is now emitted through a local emit() helper that calls both self.message_output(...) and print(..., 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.py now defines pytest_sessionstart, which walks src/usv_playpen and stores a {path: sha256} baseline on session.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_session re-hashes the same tree at session end and fails with a per-file MUTATED: / 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_modifyitems in conftest.py re-orders the collected items so test_src_integrity always 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_fields re-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: asserts AutoAddPolicy (not RejectPolicy); the ssh-rsa SHA-1 mitigation assertion is unchanged.
  • test_check_remote_mount_handles_missing_known_hosts: removed; obsolete now that AutoAddPolicy is restored.
  • test_conduct_recording_cli_invokes_controller_and_dumps_metadata: patches builtins.open for *_metadata.yaml writes.
  • 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.yaml looks empty, this release will replace it with the canonical 81-line template; no manual recovery is needed.

v0.10.3

11 May 14:22

Choose a tag to compare

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 subscriptable
    

    The crash had a clean root cause — yaml.safe_load on an empty file returns None, the defensive guard inside sync_equipment_dynamic_fields silently absorbed the non-dict, and the failure surfaced only when record_three started subscripting metadata_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.yaml and asserts it loads as a dict with Session / Equipment blocks and every Session key that record_three reads (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): drives Record → Next → Next end-to-end to actually open the Metadata window in pytest-qt, then asserts app.institution_edit.text() == app.metadata_settings['Session']['institution'] (and lab_edit likewise). The previous GUI test suite only ever reached record_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

10 May 18:03

Choose a tag to compare

Security

  • paramiko host-key validation (58ef3ba): ExperimentController.check_remote_mount now uses RejectPolicy + load_system_host_keys() instead of AutoAddPolicy. Closes the CodeQL py/paramiko-missing-host-key-validation finding 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_hosts for these hosts.
  • Legacy ssh-rsa (SHA-1) disabled (6d400cd): disabled_algorithms={"pubkeys": ["ssh-rsa"]} passed to paramiko.SSHClient.connect, dropping the deprecated SHA-1 host-key algorithm in favor of rsa-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_inputstracks.shape[0] / recording_frame_rate instead of audio_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 by generate-rm, simplifying cluster/remote transfers.

v0.10.1

10 May 01:11

Choose a tag to compare

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_features setting 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, the peak_z magnitude, 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 > 0 before normalising the mean rate R, biasing the estimate downward for sparsely-firing units. The fix computes R over all valid-occupancy bins, then sums only over bins with r > 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 anchors min on strictly-positive values only and returns linear gracefully 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_peth overlay; right half: 4×4 grid of within-USV firing rate vs continuous acoustic property.
  • Vocal page 2usv_category_tuning watershed plots over the bundled UMAP segmentation (_config/vocal_umap_segmentation.npz), plus the per-category time-resolved usv_category_peth grid.

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_cb
  • behavioral_min_occupancy_seconds, usv_property_min_occupancy_seconds
  • smoothing_sd
  • A detect_interesting_tuning_neurons block 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...
Read more

v0.10.0

03 May 03:15

Choose a tag to compare

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_files was opening a file inside a directory before that directory was created — first runs on a new probe day would have failed with an opaque FileNotFoundError. Mkdir is now hoisted before the write.
    • compute_behavioral_features.{calculate_tail_curvature, get_euler_ang} used np.reshape(..., newshape=...), deprecated in NumPy 2.1; would have started failing pytest's filterwarnings=["error"] config. Both call sites now use the positional shape= form.
  • Eight next(...) / glob()[0] lookups in assign_vocalizations.py and make_behavioral_videos.py, plus three next(key for key in dict) lookups in synchronize_files.py, replaced with first_match_or_raise(...) (or labeled KeyError). Missing-input failures now surface with FileNotFoundError: <label>: no match for glob '<pattern>' under '<root>' instead of context-free StopIteration / 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 gmmmixture_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.) from src/ and docs/. Pure-dashed lines deleted; banner-wrapped comments had dashes removed and inner text kept.
  • marimo moved from runtime dependencies to [dependency-groups].dev since runtime code does not import it.

CI

  • Workflow renamed CItests so 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 at uv 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-error so 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/w and 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_null ranges from rendering naturally).

Full Changelog: v0.9.17...v0.10.0

usv-playpen v0.9.17

27 Apr 17:40

Choose a tag to compare

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

26 Apr 14:20

Choose a tag to compare

  • 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

26 Apr 00:28

Choose a tag to compare

v0.9.14: behavioral feature math fixes — anatomical head root, reproducible back angles, corrected tail curvature, audited histogram bounds