Icca data source - #56
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>
…ht and focused on display
… to each of the source file (issue 75)
…tasource (issue 74) - find_files keeps one file per stem, preferring parquet, so a folder holding both data.csv and data.parquet no longer loads the same signals twice - 'other' now symlinks each source file into clinical_scope_output/, extending the traceability guarantee previously limited to philips_waves - an other::<stem> section can carry a trace_options block (mode, line_width, ...) overriding the datasource default - xlsx global groups emit qualified refs for other:: signals, which could not resolve before
… 74) They performed no format-specific parsing, so they were 'other' with extra machinery. Now that each file in other/ carries its own configuration and time_shift, they no longer earn a module. Example data moved into other/, configs rekeyed to other::<stem>, and a folder still named after a removed source is reported with a warning. Also corrects signal names in the demo workbook that never matched the demo data (a pre-existing mismatch this migration made visible).
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>
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>
702ecd5 to
e9666b0
Compare
AlexisJanin
left a comment
There was a problem hiding this comment.
Ported review from #52.
These are the review comments I left on #52 on 2026-06-04. That PR was opened against prepare-public-release rather than main and was closed unmerged, so the comments never reached the branch that is actually going to land. The ICCA module files are byte-identical between #52's head (1ecf569) and this branch's head (e9666b0), so every anchor maps 1:1 and all seven points below still apply to the code as it stands.
Two notes on the port:
- One comment from #52 asked for
example/template_patient_data_structure/icca/.gitkeep. That has since been added, so I have not re-posted it. - The delimiter comment originally pointed at
syringe/find_load_format.py;syringewas removed ondev(60a4adb, issue #74), so I re-pointed it atother/, which still uses the samecsv.Snifferpattern.
A separate review covering what has changed on dev since June follows.
| "anesthesia", | ||
| "icca", | ||
| ] | ||
| FILE_EXTENSIONS = [".csv", ".parquet"] |
There was a problem hiding this comment.
None-sense to have .parquet here: icca datasource can be exported in multiple format, from which we currently only support csv, and parquet is not even a possible export option from icca
Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch.
| database_options_specific, | ||
| options_naming.DATA_SOURCE_DEFAULT_TIMEZONE, | ||
| options_naming, | ||
| ) |
There was a problem hiding this comment.
Timezone localization conceptually belongs in _format, not _load — and the base _format already does it for us (it localizes whenever DATA_SOURCE_DEFAULT_TIMEZONE is defined, which it is here).
Why it matters: _load's result is written to the parquet cache, and quick_load reads that cache without calling _load. So localizing here freezes UTC into the cache, and a database_options timezone override is then silently ignored on cached runs (apply_timezone_to_dataframe skips an already-tz-aware index). Every other source caches a tz-naive frame and lets _format localize each run — the cache should have the same tz-awareness as the raw data, i.e. none.
Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch.
| logger.warning("[%s] Empty data file: %s", cls.DATASOURCE_NAME, file_path) | ||
| return pd.DataFrame( | ||
| index=pd.DatetimeIndex([], tz=options_naming.DATA_SOURCE_DEFAULT_TIMEZONE) | ||
| ) |
There was a problem hiding this comment.
Same as comment about timezone in find_load_format: return a tz-naive empty index here for consistency — _format will localize it:
return pd.DataFrame(index=pd.DatetimeIndex([]))Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch.
| df = pd.read_csv(file_path, delimiter=",", decimal=".") | ||
| else: | ||
| msg = f"Unsupported extension: '{file_path}'" | ||
| raise NotImplementedError(msg) |
There was a problem hiding this comment.
To "prepare the ground" for known future formats that may be normal for icca(e.g. xml, json, ...), keep an explicit branch that fails loudly rather than silently with an NotImplementedError. Keep this list explicit in the options file.
For format that won't ever be supported (e.g. .parquet) simply go with a format or value error (something relevant)
Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch.
| COLUMN_ATTRIBUTE_ID = "attributeId" | ||
| COLUMN_TIME = "utcmeasurementTime" | ||
| COLUMN_VALUE = "valueNumber" |
There was a problem hiding this comment.
Hardcoded constant must live in the options.py file.
Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch. Originally anchored on line 18; re-anchored to the constants block itself (15-17).
| if file_path.suffix.lower() == ".parquet": | ||
| df = pd.read_parquet(file_path) | ||
| elif file_path.suffix.lower() == ".csv": | ||
| df = pd.read_csv(file_path, delimiter=",", decimal=".") |
There was a problem hiding this comment.
delimiter="," is hardcoded; other auto-detects with csv.Sniffer. Worth matching for consistency and robustness against ;-delimited exports — see other/find_load_format.py:49-58 for the pattern.
Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch. The original pointed at syringe/find_load_format.py:26-31; the syringe source was removed on dev in 60a4adb (issue #74), so the reference now points at other/, which still carries the same sniffing pattern.
There was a problem hiding this comment.
Since the time column is literally utcmeasurementTime, a non-UTC timezone override is almost certainly a user mistake (it would localize true-UTC stamps as if they were local time). Consider warning when an override ≠ "UTC" is supplied, e.g. a small _format override that logs a warning before calling super()._format(...).
Ported from #52 (review of 2026-06-04), which was closed against the wrong base branch.
AlexisJanin
left a comment
There was a problem hiding this comment.
Second review: what has changed on dev since June.
The ICCA module is byte-identical to what was written in June against the old base (879b305). The rebase onto dev moved the three shared files — registry.py, tests/datasource/conftest.py, tutorial.md — but never touched the module itself, so it has drifted from conventions dev established in the meantime. Everything below is verified against the code as it stands, not inferred.
Two data-integrity findings worth attention before merge, both invisible in the current tests because the demo file happens not to trigger them:
aggfunc="first"drops 25% of the demo file's measurements, every collision holding two different values.- Attributes with non-numeric values are deleted at load time, so
inspect()cannot report them.
Three consistency items (deduplicate_then_sort_index, narrating comments) and four documentation gaps (README table + blurb, tutorial blurb + per-source note, CHANGELOG) follow.
Two notes on scope:
- The registry entry still carried the retired
Callable[[dict, dict | None], list[Signal]]annotation instead of theMainModulealias — that one slipped through the rebase on my side, so I've fixed it directly rather than leaving a comment. - Some of these patterns were inherited by copying
mindray_respi_numerics, which has the same issues. Where that's the case I've said so — they are not mistakes made here, and a couple deserve their own issue against the siblings rather than being fixed only in ICCA.
Things I checked that are correct and need no change: demo time alignment (ICCA lands at 10:12:38 CEST, inside the common window from 49d74ca, despite never being part of that pass); column pruning, datetime pushdown and display_fallbacks, all of which work via the base class with nothing for ICCA to declare; and the omission of ALLOW_QUICK_LOAD / CREATE_SOURCE_SYMLINK / ALLOW_DATETIME_PUSHDOWN, whose defaults are right for this source.
| df_pivoted = df.pivot_table( | ||
| index=COLUMN_TIME, | ||
| columns=COLUMN_ATTRIBUTE_ID, | ||
| values=COLUMN_VALUE, | ||
| aggfunc="first", | ||
| ) |
There was a problem hiding this comment.
aggfunc="first" is silently discarding a quarter of the demo file's measurements.
Duplicate (utcmeasurementTime, attributeId) pairs are collapsed to a single value and nothing reports it. Measured on the committed PtHighDensityAnesthesiaData_SYNTH.csv:
rows=500 unique(time,attr)=375 collapsed=125 (25%)
collision groups with >1 DISTINCT value: 125 of 125
attributeId 2423: 140 raw rows -> 79 points after the pivot
The important part is the second line: every one of the 125 collisions holds two genuinely different valueNumber values (e.g. 2423 at 08:12:54.003 carries both 80.90 and 83.28). This is not de-duplication of repeated readings — it is dropping real measurements. utcChartTime collides identically, so switching the time column doesn't help.
The docstring at lines 34-35 acknowledges the collapse, but a user has no way to learn it happened, and downstream inspect() reports the post-pivot count as if it were the file's content — so a 25%-loss export looks clean.
Two suggestions, the first cheap and worth doing regardless:
- Count and warn:
collisions = df.duplicated(subset=[COLUMN_TIME, COLUMN_ATTRIBUTE_ID]).sum() if collisions: logger.warning("[%s] %d duplicate (time, attributeId) pairs collapsed by aggfunc='first'", cls.DATASOURCE_NAME, collisions)
- Decide deliberately whether
firstis the right rule. A real export carriesvalueInstanceId,deviceSourceId,isValidandisDisplayValue(all flattened toSYNTHETIC/1in the demo file) that would normally disambiguate these. Worth checking with whoever produced the export before settling.
| # Raw column names in the ICCA PtHighDensityAnesthesiaData export. | ||
| COLUMN_ATTRIBUTE_ID = "attributeId" | ||
| COLUMN_TIME = "utcmeasurementTime" | ||
| COLUMN_VALUE = "valueNumber" |
There was a problem hiding this comment.
Attributes whose values aren't numeric disappear before inspect() can report them.
The export has 44 columns, including valueString, valueDateTime and utcValueDateTime. Reading only valueNumber means an attributeId whose values are strings — ventilator mode, drug name, a free-text remark — pivots to an all-NaN column, and pivot_table's default dropna=True then removes that column entirely:
# same frame, only the flag differs
dropna=True (current) -> pivot columns: [111, 222]
dropna=False -> pivot columns: [111, 222, 999] # 999 is the string-valued attributeWhy this one bites harder than it looks: inspect() is what users reach for to discover what's in a file, and it deliberately strips field_display upstream (base.py:756-761) precisely so it can report every column. For ICCA it can't — the attribute was destroyed inside _load, before the cache was even written. Someone configuring field_display gets "no such signal" and no way to learn the attribute exists.
The shipped demo file has zero valueString and zero valueDateTime rows, so the example data and both golden snapshots conceal this completely. It will first appear on a real export.
Suggested minimum: pass dropna=False so the attribute stays visible as an all-NaN column, and log once at load time listing the attributeIds that had a non-null valueString/valueDateTime but no valueNumber. Whether ICCA should eventually support string-valued signals is a larger design question — but the silence is the defect.
(Note: dropna=True also drops index rows that are all-NaN. Those carry no numeric data by definition, so nothing is lost that way — that half is fine.)
| # Sort by timestamp | ||
| df_pivoted = df_pivoted.sort_index() | ||
|
|
||
| # Remove duplicate timestamps (keep first) | ||
| df_pivoted = df_pivoted[~df_pivoted.index.duplicated(keep="first")] |
There was a problem hiding this comment.
Use the shared helper: deduplicate_then_sort_index from clinical_scope.io.file_utils.
1883a2c ("unify dedup-then-sort process everywhere both are used") introduced it and replaced exactly this two-liner in mindray_respi_numerics/find_load_format.py — the module ICCA was copied from — as well as in servo_u. edf and mindray_scope use it too. ICCA is the last hand-roll left.
It also runs the two steps in the opposite order to the helper, which its docstring calls out deliberately: deduplicating first keeps the first row in file order on a timestamp collision, whereas a non-stable sort_index() beforehand decides that arbitrarily.
Being straight about the scope: pivot_table already returns a unique, sorted index, so line 74 is a no-op today and no test can currently distinguish the two orders. It becomes live only if pd.to_datetime at line 68 maps two distinct index strings to the same instant — mixed formatting in the export, e.g. ...38.993000 alongside ...38.993. Note the pivot at line 57 groups by the raw string, so that's the one path where the export's text formatting leaks into grouping. Then the current order silently keeps one row and drops every attribute value that existed only in the other.
from clinical_scope.io.file_utils import deduplicate_then_sort_index
...
df_pivoted = deduplicate_then_sort_index(df_pivoted)| # Load the data | ||
| if file_path.suffix.lower() == ".parquet": | ||
| df = pd.read_parquet(file_path) | ||
| elif file_path.suffix.lower() == ".csv": | ||
| df = pd.read_csv(file_path, delimiter=",", decimal=".") | ||
| else: | ||
| msg = f"Unsupported extension: '{file_path}'" | ||
| raise NotImplementedError(msg) | ||
|
|
||
| if df.empty: | ||
| logger.warning("[%s] Empty data file: %s", cls.DATASOURCE_NAME, file_path) | ||
| return pd.DataFrame( | ||
| index=pd.DatetimeIndex([], tz=options_naming.DATA_SOURCE_DEFAULT_TIMEZONE) | ||
| ) | ||
|
|
||
| # Pivot the data: one column per attributeId (the time-series identifier) | ||
| df_pivoted = df.pivot_table( | ||
| index=COLUMN_TIME, | ||
| columns=COLUMN_ATTRIBUTE_ID, | ||
| values=COLUMN_VALUE, | ||
| aggfunc="first", | ||
| ) | ||
|
|
||
| # attributeId is an integer; use string column names so they round-trip | ||
| # through parquet and resolve as raw signal names in database_options. | ||
| df_pivoted.columns = [str(c) for c in df_pivoted.columns] | ||
|
|
||
| # Convert index to datetime | ||
| df_pivoted.index = pd.to_datetime(df_pivoted.index) | ||
|
|
||
| # Sort by timestamp | ||
| df_pivoted = df_pivoted.sort_index() | ||
|
|
||
| # Remove duplicate timestamps (keep first) | ||
| df_pivoted = df_pivoted[~df_pivoted.index.duplicated(keep="first")] | ||
|
|
||
| # Apply timezone if needed | ||
| df_pivoted = apply_timezone_to_dataframe( |
There was a problem hiding this comment.
Six comments in this method narrate what the next line does rather than explain why: # Load the data (40), # Pivot the data: … (55), # Convert index to datetime (67), # Sort by timestamp (70), # Remove duplicate timestamps (keep first) (73), # Apply timezone if needed (76).
This isn't a style preference invented for this PR — 4cfbd7e deleted the near-identical set from mindray_respi_numerics/find_load_format.py, the module this one was copied from:
- # Load the data
- # Pivot the data: one column per measurement type
- # Convert index to datetime
- # Sort by timestamp and remove duplicate timestamps (keep first)
- # Apply timezone if needed
That sibling is now down to a single comment. CLAUDE.md states the rule: "explain the non-obvious why, not the what." Line 55 also just restates docstring line 32.
Please keep lines 63-64 — "attributeId is an integer; use string column names so they round-trip through parquet and resolve as raw signal names in database_options" is exactly the kind of why that should survive; nobody would reconstruct that from the code.
|
|
||
| ## [Unreleased] | ||
|
|
||
| _Nothing yet._ |
There was a problem hiding this comment.
[Unreleased] still reads _Nothing yet._ — a new datasource is user-visible surface and needs an entry. The precedent is EDF's bullet under 1.1.0 Added (line 28):
.edfdatasource. Reads European Data Format recordings, the usual container for EEG and polysomnography exports.
Suggested:
### Added
- **ICCA datasource.** Reads Philips IntelliSpace Critical Care and Anesthesia high-density
anesthesia exports, pivoting the long-format rows into one signal column per `attributeId`.| | Mindray Scope | Mindray monitor | `.xml`, `.csv` | ECG, SpO₂, pressure waveforms | | ||
| | Mindray Respi Waves | Mindray respiratory | `.parquet`, `.csv` | High-frequency respiratory waveforms | | ||
| | Mindray Respi Numerics | Mindray respiratory | `.parquet`, `.csv` | Vt, RR, PEEP, and more | | ||
| | EDF / EDF+ | Amplifiers and polygraphic recorders | `.edf` | Any EDF-exported signal, typically EEG | |
There was a problem hiding this comment.
The Supported Data Sources table has no ICCA row. All nine other registered sources are listed, and EDF got its row when it was added — this table is one of the places that rots silently when a source is added.
Suggested insertion after this line (before Other (Generic), matching DataSource.AVAILABLE order):
| ICCA | Philips IntelliSpace CCA anesthesia export | `.csv`, `.parquet` | Anesthesia high-density signals (HR, SpO₂, ABP/NIBP, airway pressures) |Note the extension column here should stay in sync with options.py's FILE_EXTENSIONS and with the tutorial's table row — see the separate thread about whether .parquet belongs in that list at all.
| --- | ||
|
|
||
| **ClinicalScope** is an open-source, browser-based dashboard for visualizing, annotating, and extracting time-series data. Its primary domain is ICU monitoring — loading recordings from multiple clinical devices simultaneously (Philips monitors, Servo-U ventilators, EIT systems, FluxMed, Mindray, syringe pumps) — but its annotation and extraction pipeline is designed for any time-series data, making it equally useful for machine learning workflows that require labeled datasets. | ||
| **ClinicalScope** is an open-source, browser-based dashboard for visualizing, annotating, and extracting time-series data. Its primary domain is ICU monitoring — loading recordings from multiple clinical devices simultaneously (Servo-U ventilators, EIT systems, FluxMed, Mindray, EDF recorders, plus a generic reader for any tabular export — monitors, syringe pumps, and the like) — but its annotation and extraction pipeline is designed for any time-series data, making it equally useful for machine learning workflows that require labeled datasets. |
There was a problem hiding this comment.
This intro blurb lists the devices by name — "Servo-U ventilators, EIT systems, FluxMed, Mindray, EDF recorders" — and ICCA is missing. The canonical table further down is the thing that must be right, but this sentence is what a new reader actually sees first.
Same omission in docs/user_guide/tutorial.md:25 ("Display signals from FluxMed, Mindray, EIT, Servo-U and EDF recorders simultaneously") — flagged separately there.
| generic "Other" source for any CSV/parquet). | ||
| - **Generic "Other" data source**: Drop any CSV or Parquet file with a datetime column | ||
| into an `other/` folder — signals are auto-discovered and can be configured per file. | ||
| - **Multi-source visualization**: Display signals from FluxMed, Mindray, EIT, Servo-U and EDF recorders simultaneously. |
There was a problem hiding this comment.
"Display signals from FluxMed, Mindray, EIT, Servo-U and EDF recorders simultaneously" — ICCA needs adding to this list. (Same omission in README.md:28.)
| - **A date alone** (`2024-10-08`) shifts the recording by whole days and keeps the file's own time of day — use this when only the date was scrubbed. | ||
| - Times are read in the **device's** timezone (the source's `additional_informations.timezone`, `Europe/Paris` by default), not your display timezone. | ||
|
|
||
| A file that still carries a real start date keeps it, and `recording_start` is ignored. A file with no date and no `recording_start` is still plotted, anchored at 1985-01-01, with a warning in the log. |
There was a problem hiding this comment.
ICCA needs a per-source note here, after the EDF one. Of everything in this review this is the one I'd argue hardest for, because without it the source is close to unusable for a clinician.
ICCA's raw signal names are stringified integer attributeIds — 2423, 19602, 4347. There is no name dictionary anywhere in the export: terseForm and verboseForm only repeat the value and its unit. So someone opening an ICCA file has no way to guess what to put in field_display or signals, and no way to find out from the file itself. Every other source in this guide has human-readable raw names, so nothing prepares a reader for this.
Two facts to cover, mirroring the shape of the EDF note above:
- Signal names are
attributeIdintegers, as strings. Point readers atinspect(or the demo config atexample/demo_database/database_options.json) to discover which ids a given export contains, and note that mapping an id to a clinical meaning needs the site's own ICCA dictionary. - Timestamps come from
utcmeasurementTime, i.e. UTC, which is why this source defaults to UTC rather thanEurope/Parislike the others — overridable viaadditional_informations.timezone.
Worth saying explicitly that the ids are opaque by design of the export format, so nobody files it as a bug.
A new new beginning for ICCA vizualisation
Add support for ICCA datasource