Release v1.1.0: spectrogram/PSD, EDF, user options, per-file other:: config - #64
Conversation
…ere both are used
Improve loading time and memory
display_timezone's only load-path job was interpreting a naive datetime_start/ datetime_end; storing the bounds as tz-aware instants instead removes that job entirely, so display_timezone becomes a pure render concern (unblocks #69). - timezone.py: to_aware_display_ts (inverse of to_naive_display_ts) plus a shared resolve_display_timezone coercion so a hand-edited or programmatic bad timezone name falls back and logs instead of raising inside pandas. - Submit bakes the form's naive text into an aware instant before saving; Reload renders it back as naive wall-clock in the saved file's own display_timezone; a new callback rewrites the datetime fields when display_timezone itself is edited, so the instant survives the change. - eit/find_load_format.py: strip the offset before treating datetime_start as the inferred recording day, preserving today's calendar-day behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract shared type-check and window/overlap-override boilerplate from Signal.spectrogram_from_signal/psd_from_signal; move hovertemplate dB/Hz precision into constants.Spectral; drop an issue-number reference from a comment; close test gaps around the sentinel-only-column warning loop (only trace_mode was covered; timezone had no coverage at all), trace_options isolation across datasources, and characterize today's unvalidated pass-through of trace_mode/line_width/opacity. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
example/ mixed shipped demo material with test-only fixtures. It now holds
only what a user or the release bundle touches; test fixtures moved under
tests/data/.
Moves (history preserved):
- Patient_difficult_format -> tests/data/patients/
- example_database_options.{json,xlsx} -> tests/data/option_files/
Removals:
- Patient_other: unreferenced; two files were byte-identical to demo_patient,
and its stem-collision case is already covered synthetically in test_other.py
- example_database_options_other.{json,xlsx}: superseded by the demo config
- template_database_options.json: restated the tutorial reference with no
consumer and had silently drifted three features behind
example/demo_database/database_options.json is new: a generated twin of the
xlsx, so library users who never launch the app have a runnable config to
point at. The xlsx gained sections for fluxmed_signals, fluxmed_parameters,
mindray_respi_waves and mindray_respi_numerics -- the demo shipped data for
these but never plotted them -- plus range/visible/display coverage. Every
column of the signals sheet is now exercised by a real example.
New guards in tests/unit/test_example_assets.py: the folder template matches
the registry (this is how edf/ went missing), each template folder keeps its
.gitkeep, the demo json matches its xlsx, and the demo config configures every
datasource it ships.
Docs: tutorial repointed at the demo configs and its display/visible rows now
state how they differ; CLAUDE.md corrects the database_options key name to
'signals'; README names a real config path; the new-datasource skill requires
a demo config section; the organize-patient-folder evals no longer target a
patient folder deleted a release ago.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The demo's anonymisation left two stamping conventions in the raw files (naive 08:12:33 and 06:12:33 UTC), and each source's DATA_SOURCE_DEFAULT_TIMEZONE read them differently, so one recording landed in three clusters two hours apart. Loading the demo showed sources that never overlapped. Data shifted +2h so it agrees with the sources that were already right: - mindray_scope: all seven CSVs re-stamped; filenames renamed to keep the range they advertise truthful (they only supply the signal name to the loader, which reads timestamps from the file contents) - servo_u: the 'Log start' header of Curves_00000000.sta, which is the sole reference -- timestamps are built as Log start + Time(ms), and the T(h:m:s.ms) column is dropped - other/numerics.parquet: tz-aware index 06:12:33Z -> 08:12:33Z, since an already-aware index ignores the database_options timezone override Config instead of data where the stored index is naive: other::waves moves from Europe/Paris to UTC. The demo now exercises four timezone paths at once -- explicit UTC, explicit non-European (eit stays Asia/Karachi, landing 21 minutes after the others), implicit source default, and already-aware-so-override-ignored. Ten of eleven sources now start at 10:12:33 CEST; eit remains offset so the demo still has something for time_shift to correct. patient_options_example.json and the README quickstart now carry the demo's real eit day and edf recording_start, so 'try it immediately' holds. Snapshots regenerated for mindray_scope and servo_u. The naive/aware bound equivalence test pinned two windows seven seconds into the servo_u recording; both move +2h with the data, keeping the same instant expressed two ways. Full suite green: 809 passed, ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spectrogram & PSD plot types, EEG datasource, other-source cleanup
CI installs ruff unpinned and had moved to 0.16.2 while local venvs sat on 0.15.x, so the format step failed on four files and had masked the lint and test steps entirely — they had never run on this branch. - Reformat the three Python files that no ruff version accepted, one of which also broke E501, plus build_info/README.md: ruff 0.16 formats Python code blocks inside Markdown, which no 0.15 release did. - Ignore CPY001. `select = ["ALL"]` swept in the new missing-copyright rule and it fired on all 66 modules; attribution lives in LICENSE, not per-file. - Parenthesise the implicit string concatenations in the native-license map. Adjacent strings sat beside real tuple elements in a dict[str, tuple[str, str]], where a missing comma would silently widen the tuple and corrupt attribution. - Drop ten `# noqa: BLE001` directives that 0.16 made dead: it no longer flags a broad except whose handler logs with exc_info. Verified with ruff 0.16.2: format --check and check both clean; full suite passes on Python 3.11.15 and 3.13.13 (841 tests each). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The [Unreleased] section only covered the datasource removal (#74/#75); the other ~40 commits since v1.0.0 were undocumented. Write them up and cut the release entry. Bump to 1.1.0 in both pyproject.toml and CITATION.cff — the release checklist tracks both, and CITATION.cff had drifted, still reading 1.0.0 against a pyproject already at 1.0.1. 1.1.0 rather than 2.0.0 for the datasource removal: the break is real but narrow, and the migration is a rename inside two config files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both are interactions between the new user_options tier and code that predates it, and both fail silently. Hovermode: render_annotations patched layout.hovermode to a hardcoded literal while PlotModel.to_figure sets it from DisplayFallbacks. The callback fires on any annotation create/edit/delete or mode toggle, so choosing "Closest point only" in Settings held until the user touched an annotation, then reverted for the rest of the session. The comment claimed the two stayed in step; the capability set was shared, the value was not. Read it from the store instead — point mode still forces "closest", which is what placing a point needs. Spectrogram dB range: each bound was clamped independently with no ordering check, so a min above max survived to go.Heatmap as zmin > zmax. Reachable by typing two valid numbers into Settings (100 then 40), and also by the clamp itself, which turns an out-of-range pair into (100.0, -100.0). Add ordered_bounds, which falls back to the schema defaults and says so in the log. Also close the FLOAT hole in _coerce: it clamped INT only, while its docstring promised "numeric fields are clamped". The two FLOAT options are exactly the spectrogram bounds above. Adds 7 tests, including the positive cases so the fallbacks cannot pass vacuously. Full suite 848 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An ADR audit over the 49 commits found the reasoning was not skipped, it went into issue bodies instead — #57 literally opens "This issue is the decision record + spec". Those issues are closed and #57 has already drifted, since it specifies pushdown for philips_waves, which this same branch deletes. Three decisions cleared the bar (a real alternative was available and rejected, and the decision constrains future work): - 0007, read-time pruning is an optimization, never a filter. The rejected alternative was exact pushdown, which loses rows silently because time_shift and timezone resolution run after load. Also records why inspect does not push down: its "% retained" is a comparison against the unwindowed file, so pushing the window into the read makes the number a tautology. - 0008, a datasource module is justified only by format-specific parsing. The criterion was in CLAUDE.md as bare fiat; CONTRIBUTING.md now carries it too, which is where a contributor actually looks before writing one. - 0009, other::<stem> is a config scope and :: is the qualified-name separator. Records why the per-file scope is deliberately NOT generalised to the other sources, whose folders hold chunks of one recording rather than unrelated files. Decisions deliberately left without an ADR: the capability-set PlotType design, the edf addition, the example/ split, demo time alignment, source symlinks and stem dedup — mechanical, precedent-following, or already covered. Also fix the PSD example in the tutorial: db_range [40, 90] predates the spectral density re-scale and now sits entirely above the data, which measures 6.5-41 dB on the demo EEG. [0, 40] matches the two spectrogram examples the re-scale did update. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three-axis review —
|
| Axis | Findings | Worst in that axis |
|---|---|---|
| Standards | 9 | Hand-spelled Dash callback IDs that break silently on rename |
| Spec | 9 (7 accepted) | PSD db_range documented above the data — now fixed |
| Maintainability | 8 | Hovermode silently discarding a user setting — now fixed |
The three I'd fix first, from anywhere: the trace_options accept/interpret gap (ten scopes accept, one reads); the hand-spelled user_options.display_timezone callback IDs; and the tests that restate their own constants, which are the ones least likely to be caught by anything else.
Merge readiness: CI green on both matrix Pythons, 848 tests passing, no ADR violated, breaking change documented with a migration path. Nothing outstanding blocks the merge.
|
Follow-ups from the review above are now tracked:
The 📋 markers in the review map onto these three. |
Pass over the 48 Python files this release touched. 935 comments examined, 13 removed, 16 rewritten, 906 kept — the codebase's comment density is mostly earned, so the bar was "delete only what restates the line below it". Removed the repo issue tags per house rule, rewriting each to carry its reason in prose instead of behind a link: base.py (x2), constants.py, eit/options.py, registry.py, bench_pushdown.py (x3). "see issue #53" became "one diagnostic per patient would flood the log". Two worth calling out: - constants.py had a commented-out fake `class DataSourceRelative` standing in for documentation. Replaced with a real one-line pointer to where per-datasource patient options actually live. The knowledge survived. - eit/options.py asked for names to move to example_eit_options.json, a file that exists nowhere in the repo — an aspiration pointing at something that never shipped. Deliberately kept: the dash PR #3785 reference (upstream, and the only way to know when the zoom workaround can go), the tz-naive/tz-aware folklore in mindray_scope, and ~90 section banners, which are a house convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Most markdown here grew substantially during 1.1.0 and had started repeating
itself across files. 106 lines removed, no facts dropped.
Cross-file duplication resolved by deferring to the canonical source:
- README restated the tutorial's "canonical" datasource table one line above a
link to it. Kept device/format/extensions as front-page orientation, dropped
the folder-keyword column to the tutorial.
- CONTRIBUTING was linked from README three times over — prose line, a
Documentation table, and its own section. The table went.
- The module-justification rule sat in CONTRIBUTING, CLAUDE.md and ADR-0008.
The summaries keep the test and a link; the rationale lives only in the ADR.
- output_root's one-root-per-database warning appeared three times in the
tutorial; the explanation stays in one place, the rest cross-reference.
One substantive correction, verified against the code: the `numerics` block was
documented as applying to "all numeric parameter signals". It is read in
_build_trace_options (signal_container.py:421), which runs for every signal with
no filtering — the name misleads and the docs repeated the mistake. Fixed in the
prose and in both table rows that echoed it.
Also fixed three typos that were factual noise ("providem uch", "runned") and
dropped a machine-specific `cd ~/Codes/ClinicalScope` from tests/README.md.
Left long on purpose: the `::` shadowing paragraph, the spectral-density
rescale, and CHANGELOG.md entirely — its migration notes are the only path
users get. The ADRs needed no cuts; their length is the rejected-alternatives
reasoning the format exists to carry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_load's output is the parquet cache, so anything it resolves from configuration outlives the setting that produced it. ADR-0010 states the general rule, its silent-staleness failure mode, the audit that found three violations plus two latent ones, and the ruling that a mindray_scope folder mixing .csv and .xml now fails loudly instead of being rescued by the violation. The /new-datasource skill taught the opposite for empty frames; both its occurrences now show a naive DatetimeIndex. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five loaders stamped a timezone into their output, and eit narrowed its parser on field_display — all of it frozen into the parquet cache and unreachable from a later run's config (ADR-0010). - fluxmed_parameters / fluxmed_signals: the filename stamp stays naive; _format localizes it to the same UTC as before. - mindray_scope: the .csv branch stays naive, and a folder yielding both naive .csv and offset-bearing .xml now raises with both file names instead of being silently rescued by the localization. - mindray_respi_numerics / mindray_respi_waves: only the file's own offset reaches the index; the DATA_SOURCE_DEFAULT_TIMEZONE fallbacks are gone. - eit: the whole columns_to_extract chain is dead, so _parse_asc_table parses every column. test_load_config_independence.py calls each caching source's _load twice with configs differing in field_display and the timezone override and asserts the frames match. It failed on eit and mindray_scope before these edits. Three loaded.parquet goldens move by exactly one tz strip — same wall clock, columns and values. Every formatted.parquet is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
read_parquet_pruned gated column pruning on the stored index being a timestamp, so any cache with a non-temporal index fell back to reading every column. The gate is now provenance rather than dtype: _quick_load, the only caller reading a file we wrote ourselves, declares that its index is the time axis. Every other caller keeps today's behaviour. resolve_stored_datetime_index becomes resolve_stored_index_field, which answers "which column is the index" and nothing else. Whether that index is a range-comparable time axis is a second question, now answered on its own line at the call site: a declared axis prunes columns and can never carry a row predicate, so the pushdown opt-out is structural. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
%Local N = Local N / Global resolves no option, so it is transcription rather than interpretation and belongs in _load. Computing it there puts the ratios in the parquet cache, where a field_display naming %Local 1* now matches a real column instead of nothing: a configured EIT read goes from all 71 columns to 9 of 75. _format no longer re-derives them. On a cache written before this change the % columns are absent and pruning selects nothing for them, so a back-fill would have worked only when Global happened to be selected too. Caches are disposable instead: un-tick "Re-use data if already loaded once" once after an update and the next run writes a complete one. Also drops _add_columns_percentage, dead since the EIT-specific variant replaced it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The changelog was written on 2026-08-12 and the branch kept moving: ADR-0010 and the loader-purity work that followed it are user-visible. - release date and CITATION.cff date-released move to the merge date - an upgrading note: this release changes what the parquet cache holds and nothing detects a cache written by an older version, so quick_load must be un-ticked once per patient - the mindray_scope csv+xml folder now failing loudly is a behaviour change, not an internal one - EIT %Local columns and the non-timestamp-index pruning fix belong under Performance - seven ADRs became eight Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merges the accumulated
devwork intomainas v1.1.0 — 64 commits since v1.0.0, touching 175 files (+16,957 / −7,775).What's in it
Two new plot types. Spectrogram and PSD, for high-rate signals (EEG, pressure waveforms). Both are re-renderings of the same samples, which is the side of the ADR-0006 line this library stays on.
A
.edfdatasource — European Data Format, the usual EEG/polysomnography container.A third configuration tier,
user_options. Per-person app behaviour and display fallbacks, edited in the Settings modal. It only ever fills gapsdatabase_optionsleft — it never overrides (ADR-0005).Per-file configuration for
other/. Each file gets its ownother::scope with its owntime_shift, timezone, grouping and trace style (ADR-0009).Breaking:
philips_waves,philips_numericsandsyringeare removed. They did no format-specific parsing, so onceother/gained per-file scopes they no longer earned a module (ADR-0008). The changelog carries the migration, including three traps: the syringe timezone default moves Paris → UTC, marker traces need restoring viatrace_options, and signals take the qualifiedother::<stem>::<column>form.Substantially faster loading. Columns are selected and pruned before reading, with row pushdown as a strictly optional accelerator (ADR-0007).
Full detail in CHANGELOG.md.
This release changes what
clinical_scope_output/holds, and nothing detects a cache written by an older version. After updating, un-tick "Re-use data if already loaded once" (quick_load) for one run per patient. A stale cache gives EIT recordings without their%Localcolumns, and timestamps carrying whatever timezone the old run happened to stamp in.Pre-merge hardening on this PR
CI had been red since 2026-07-29 — 13 consecutive failures. Two independent causes, both in
ruff format --check:E501).dev = ["ruff"]is unpinned, so CI had moved to ruff 0.16.2 while local venvs sat on 0.15.x. 0.16 formats Python code blocks inside Markdown (a 4th file) and, underselect = ["ALL"], addedCPY001— 66 hits across every module.Because the format step fails first and
fail-fast: truecancels the sibling job, lint and tests had never once run on this branch. They do now: 848 tests pass on Python 3.11 and 3.13, and both matrix jobs are green on the merge head.Also landed here:
user_optionsand code predating it — see the review comment below.db_rangein the tutorial corrected against measured demo data.pyproject.tomlandCITATION.cff; the two had drifted (1.0.1 vs 1.0.0) anddocs/RELEASING.mdtracks both.After the review: loader purity (ADR-0010)
An audit run after the three-axis review found loaders resolving configuration — a timezone stamped in by five of them, and
eitnarrowing its parser onfield_display. Because_load's output is the parquet cache, any such value outlives the setting that produced it and is unreachable from a later run's config. ADR-0010 states the rule:_loadtranscribes,_formatinterprets._formatlocalizes to the same instants as before.test_load_config_independence.pycalls each caching source's_loadtwice under differing configs and asserts the frames match.mindray_scopefolder mixing naive.csvand offset-bearing.xmlnow raises with both file names, instead of being silently rescued by the default timezone.%Local Nratios move into_load— they resolve no option, so they are transcription. That puts them in the cache wherefield_displaycan select them: a configured EIT read goes from all 71 columns to 9 of 75.This is the work that makes the cache-rewrite note above necessary.
Review
A three-axis review (Standards / Spec / Maintainability) was run over the full
main...devdiff; findings and their triage are in the review comment on this PR. The deferred findings are tracked in #79, #80 and #81.Issues shipped here
#53, #57, #63, #66, #68, #69, #71, #72, #73, #74, #75 — all already closed except #71 (spectrogram), which is labelled
status: donebut still open. No closing keywords are used in this body, so merging changes no issue state.