Skip to content

Commit 9c3d125

Browse files
committed
Add normalization for missing strings in DICOM tags snapshot and enhance tests
1 parent 3e98ef6 commit 9c3d125

3 files changed

Lines changed: 89 additions & 1 deletion

File tree

src/imperandi/ingest/parse.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,21 @@ def read_dicom_header_with_force(fp, force):
550550
return read_dicom_header(fp, force=force)
551551

552552

553+
def _normalize_snapshot_missing_strings(value):
554+
if isinstance(value, str):
555+
return None if value.strip() == "" else value
556+
if isinstance(value, list):
557+
return [_normalize_snapshot_missing_strings(v) for v in value]
558+
if isinstance(value, tuple):
559+
return [_normalize_snapshot_missing_strings(v) for v in value]
560+
if isinstance(value, dict):
561+
return {
562+
key: _normalize_snapshot_missing_strings(val)
563+
for key, val in value.items()
564+
}
565+
return value
566+
567+
553568
def build_global_readers(
554569
*,
555570
initial_archive_mode: bool,
@@ -653,7 +668,7 @@ def write_dicom_tags_snapshot(
653668
"_relative_path": row.get("_relative_path"),
654669
"snapshot_seed": seed,
655670
"snapshot_index": int(idx),
656-
"tags": tags_series.to_dict(),
671+
"tags": _normalize_snapshot_missing_strings(tags_series.to_dict()),
657672
}
658673
handle.write(json.dumps(record, ensure_ascii=True) + "\n")
659674
written += 1

tests/test_clean.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010

1111
from imperandi.ingest import clean
1212

13+
ACQUISITION_TEMP_COLS = {
14+
"_acq_timestamp",
15+
"_series_number_sort",
16+
"_acquisition_number_sort",
17+
}
18+
19+
20+
def _assert_no_acquisition_temp_cols(df: pd.DataFrame) -> None:
21+
assert ACQUISITION_TEMP_COLS.isdisjoint(df.columns)
22+
1323

1424
def test_normalize_clean_args_prefers_optional_csv_path(tmp_path):
1525
csv_pos = tmp_path / "pos.csv"
@@ -344,6 +354,7 @@ def test_compute_visit_and_acquisition_order():
344354
assert set(out2["acquisition_order"].dropna()) == {0, 1, 2}
345355
assert (out2["delay_since_prev_acq_sec"].dropna() >= 0).all()
346356
assert (out2["delay_since_first_acq_sec"].dropna() >= 0).all()
357+
_assert_no_acquisition_temp_cols(out2)
347358

348359
# Handles aggregated time values represented as datetime.time objects or repr strings
349360
df3 = pd.DataFrame(
@@ -367,6 +378,7 @@ def test_compute_visit_and_acquisition_order():
367378
assert out3.set_index("volume_id").loc["v3", "acquisition_order"] == 2
368379
assert (out3["delay_since_prev_acq_sec"].dropna() >= 0).all()
369380
assert (out3["delay_since_first_acq_sec"].dropna() >= 0).all()
381+
_assert_no_acquisition_temp_cols(out3)
370382

371383
# Ensure ordering uses acquisition timestamp, not lexical volume_id order.
372384
df4 = pd.DataFrame(
@@ -391,6 +403,7 @@ def test_compute_visit_and_acquisition_order():
391403
assert out4_by_volume.loc["v1", "acquisition_order"] == 2
392404
assert (out4["delay_since_prev_acq_sec"].dropna() >= 0).all()
393405
assert (out4["delay_since_first_acq_sec"].dropna() >= 0).all()
406+
_assert_no_acquisition_temp_cols(out4)
394407

395408

396409
def test_compute_acquisition_order_without_time_uses_series_and_acquisition_number():
@@ -413,6 +426,7 @@ def test_compute_acquisition_order_without_time_uses_series_and_acquisition_numb
413426
assert out_by_volume.loc["v2", "acquisition_order"] == 0
414427
assert out_by_volume.loc["v3", "acquisition_order"] == 1
415428
assert out_by_volume.loc["v1", "acquisition_order"] == 2
429+
_assert_no_acquisition_temp_cols(out)
416430

417431

418432
def test_compute_acquisition_order_without_date_and_time_falls_back_to_numbers():
@@ -433,6 +447,7 @@ def test_compute_acquisition_order_without_date_and_time_falls_back_to_numbers()
433447
assert out_by_volume.loc["v2", "acquisition_order"] == 0
434448
assert out_by_volume.loc["v3", "acquisition_order"] == 1
435449
assert out_by_volume.loc["v1", "acquisition_order"] == 2
450+
_assert_no_acquisition_temp_cols(out)
436451

437452

438453
def test_compute_acquisition_order_tie_breaks_by_volume_id_when_no_sort_keys():
@@ -450,6 +465,28 @@ def test_compute_acquisition_order_tie_breaks_by_volume_id_when_no_sort_keys():
450465
assert out_by_volume.loc["v1", "acquisition_order"] == 0
451466
assert out_by_volume.loc["v10", "acquisition_order"] == 1
452467
assert out_by_volume.loc["v2", "acquisition_order"] == 2
468+
_assert_no_acquisition_temp_cols(out)
469+
470+
471+
def test_compute_acquisition_order_drops_internal_sort_columns():
472+
df = pd.DataFrame(
473+
{
474+
"patient_key": ["p", "p"],
475+
"study_id": ["s", "s"],
476+
"volume_id": ["v1", "v2"],
477+
"date": [pd.Timestamp("2020-01-01"), pd.Timestamp("2020-01-01")],
478+
"time": [dt_time(12, 0, 0), dt_time(12, 1, 0)],
479+
"SeriesNumber": [1, 1],
480+
"AcquisitionNumber": [1, 2],
481+
}
482+
)
483+
484+
out = clean.compute_acquisition_order(df.copy())
485+
486+
_assert_no_acquisition_temp_cols(out)
487+
assert "acquisition_order" in out.columns
488+
assert "delay_since_prev_acq_sec" in out.columns
489+
assert "delay_since_first_acq_sec" in out.columns
453490

454491

455492
def test_group_volumes_sorts_acquisition_number_numerically():

tests/test_parse.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,39 @@ def test_write_dicom_tags_snapshot_samples_unique_series_when_available(tmp_path
686686
assert n == 4
687687
assert len(lines) == 4
688688
assert len({item["tags"]["Source"] for item in lines}) == 4
689+
690+
691+
def test_write_dicom_tags_snapshot_normalizes_missing_empty_strings(tmp_path):
692+
df = pd.DataFrame(
693+
{
694+
"dicom_path": ["p0.dcm"],
695+
"_scan_root": ["root"],
696+
"_relative_path": ["rel/0.dcm"],
697+
"_read_path": ["read/0.dcm"],
698+
}
699+
)
700+
701+
out = tmp_path / "snap_empty.ndjson"
702+
n = parse.write_dicom_tags_snapshot(
703+
df=df,
704+
output_path=out,
705+
sample_size=1,
706+
seed=42,
707+
read_full_func=lambda _: pd.Series(
708+
{
709+
"EmptyTag": "",
710+
"WhitespaceTag": " ",
711+
"Nested": ["", "ok"],
712+
"PresentTag": "value",
713+
}
714+
),
715+
)
716+
717+
assert n == 1
718+
line = out.read_text(encoding="utf-8").strip()
719+
record = json.loads(line)
720+
tags = record["tags"]
721+
assert tags["EmptyTag"] is None
722+
assert tags["WhitespaceTag"] is None
723+
assert tags["Nested"] == [None, "ok"]
724+
assert tags["PresentTag"] == "value"

0 commit comments

Comments
 (0)