euler-train 2.5.0__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -18,7 +18,7 @@ def save_output_tree(
18
18
  format_overrides: dict[str, str],
19
19
  visualization_overrides: dict[str, Any],
20
20
  output_type: str,
21
- ) -> None:
21
+ ) -> dict[str, Any]:
22
22
  """Persist all slots (pred, gt, input, aux/…) for one *output_type*.
23
23
 
24
24
  *slots* example::
@@ -28,7 +28,10 @@ def save_output_tree(
28
28
  "gt": array,
29
29
  "aux": {"transmission": array, "attention": array},
30
30
  }
31
+
32
+ Returns a dict mapping slot keys to their manifest entries.
31
33
  """
34
+ manifest: dict[str, Any] = {}
32
35
  for slot_name, data in slots.items():
33
36
  if data is None:
34
37
  continue
@@ -36,7 +39,7 @@ def save_output_tree(
36
39
  for aux_name, aux_data in data.items():
37
40
  if aux_data is None:
38
41
  continue
39
- _save_slot(
42
+ manifest[f"aux/{aux_name}"] = _save_slot(
40
43
  type_dir / "aux" / aux_name,
41
44
  aux_data,
42
45
  output_type,
@@ -45,7 +48,7 @@ def save_output_tree(
45
48
  visualization_overrides,
46
49
  )
47
50
  else:
48
- _save_slot(
51
+ manifest[slot_name] = _save_slot(
49
52
  type_dir / slot_name,
50
53
  data,
51
54
  output_type,
@@ -53,6 +56,7 @@ def save_output_tree(
53
56
  format_overrides,
54
57
  visualization_overrides,
55
58
  )
59
+ return manifest
56
60
 
57
61
 
58
62
  # ---------------------------------------------------------------------------
@@ -66,7 +70,8 @@ def _save_slot(
66
70
  leaf_name: str,
67
71
  format_overrides: dict[str, str],
68
72
  visualization_overrides: dict[str, Any],
69
- ) -> None:
73
+ ) -> dict[str, Any]:
74
+ """Save items for one slot and return a manifest entry."""
70
75
  slot_dir.mkdir(parents=True, exist_ok=True)
71
76
  visualization = _resolve_visualization(
72
77
  output_type, leaf_name, visualization_overrides,
@@ -74,16 +79,21 @@ def _save_slot(
74
79
 
75
80
  # Dict with string keys → named outputs (e.g. {"scene_042": img}).
76
81
  if isinstance(data, dict):
82
+ files: list[dict[str, Any]] = []
77
83
  for name, raw in data.items():
78
84
  item = _prepare(raw)
79
85
  fmt = _resolve_format(item, output_type, leaf_name, format_overrides)
80
86
  _save_item(slot_dir / f"{name}.{fmt}", item, fmt, visualization)
81
- return
87
+ files.append({"sample_id": name, "filename": f"{name}.{fmt}", "format": fmt})
88
+ return {"id_mode": "named", "files": files}
82
89
 
83
90
  items = _unpack(data)
91
+ files = []
84
92
  for idx, item in enumerate(items):
85
93
  fmt = _resolve_format(item, output_type, leaf_name, format_overrides)
86
94
  _save_item(slot_dir / f"{idx:04d}.{fmt}", item, fmt, visualization)
95
+ files.append({"sample_id": idx, "filename": f"{idx:04d}.{fmt}", "format": fmt})
96
+ return {"id_mode": "indexed", "files": files}
87
97
 
88
98
 
89
99
  # ---- normalisation -------------------------------------------------------
@@ -361,6 +361,7 @@ class Run:
361
361
  *,
362
362
  epoch: int | None = None,
363
363
  step: int | None = None,
364
+ metadata: dict[str, Any] | None = None,
364
365
  **output_types: dict[str, Any],
365
366
  ) -> Path:
366
367
  """Save prediction / ground-truth / auxiliary arrays to disk.
@@ -383,11 +384,26 @@ class Run:
383
384
  rgb=dict(pred={"scene_042": img_a, "scene_117": img_b}),
384
385
  )
385
386
 
387
+ An optional *metadata* dict attaches source context (dataset,
388
+ split, etc.) to the manifest. The ``dataset`` key is required
389
+ when *metadata* is provided::
390
+
391
+ run.save_outputs(
392
+ epoch=1, step=500,
393
+ metadata={"dataset": "vkitti2", "split": "val"},
394
+ rgb=dict(pred=pred_img),
395
+ )
396
+
386
397
  Accepted slot keys: ``pred``, ``gt``, ``input``, ``aux``.
387
398
  ``aux`` expects a sub-dict of named arrays.
388
399
 
389
400
  Returns the directory that was written to.
390
401
  """
402
+ if metadata is not None:
403
+ if not isinstance(metadata.get("dataset"), str) or not metadata["dataset"].strip():
404
+ raise ValueError(
405
+ "metadata must include a non-empty 'dataset' string"
406
+ )
391
407
  parts: list[str] = []
392
408
  if epoch is not None:
393
409
  parts.append(f"epoch_{epoch}")
@@ -396,21 +412,38 @@ class Run:
396
412
  dirname = "_".join(parts) or "unspecified"
397
413
  base = self.dir / "outputs" / dirname
398
414
 
415
+ manifest_types: dict[str, Any] = {}
399
416
  for output_type, slots in output_types.items():
400
417
  if slots is None:
401
418
  continue
402
- save_output_tree(
419
+ slot_manifest = save_output_tree(
403
420
  base / output_type,
404
421
  slots,
405
422
  self._output_formats,
406
423
  self._output_visualization,
407
424
  output_type,
408
425
  )
426
+ if slot_manifest:
427
+ manifest_types[output_type] = slot_manifest
428
+ if manifest_types:
429
+ manifest: dict[str, Any] = {
430
+ "version": 1,
431
+ "epoch": epoch,
432
+ "step": step,
433
+ "output_types": manifest_types,
434
+ "format_overrides": dict(self._output_formats),
435
+ "visualization_overrides": {
436
+ k: dict(v) if isinstance(v, Mapping) else v
437
+ for k, v in self._output_visualization.items()
438
+ } if self._output_visualization else {},
439
+ }
440
+ if metadata is not None:
441
+ manifest["metadata"] = metadata
442
+ write_json(base / "manifest.json", manifest)
409
443
  if base.exists():
410
444
  self._record_artifact_update(base)
411
445
  self._flush_meta()
412
- snapshot = self._build_output_snapshot(epoch, step, output_types)
413
- if snapshot is not None:
446
+ if manifest_types and epoch is not None and step is not None:
414
447
  self._emit_stream_event(
415
448
  {
416
449
  "type": "output_snapshot",
@@ -419,7 +452,13 @@ class Run:
419
452
  epoch,
420
453
  step,
421
454
  ),
422
- "snapshot": snapshot,
455
+ "snapshot": {
456
+ "version": 1,
457
+ "epoch": epoch,
458
+ "step": step,
459
+ "output_types": manifest_types,
460
+ **({"metadata": metadata} if metadata is not None else {}),
461
+ },
423
462
  },
424
463
  )
425
464
  return base
@@ -888,44 +927,6 @@ class Run:
888
927
  ]
889
928
  return tags or None
890
929
 
891
- def _build_output_snapshot(
892
- self,
893
- epoch: int | None,
894
- step: int | None,
895
- output_types: dict[str, Any],
896
- ) -> dict[str, Any] | None:
897
- if epoch is None or step is None:
898
- return None
899
-
900
- snapshot_types: dict[str, list[str]] = {}
901
- for output_type, slots in output_types.items():
902
- if slots is None or not isinstance(slots, Mapping):
903
- continue
904
-
905
- categories: list[str] = []
906
- for slot_name, data in slots.items():
907
- if data is None:
908
- continue
909
- if slot_name == "aux" and isinstance(data, Mapping):
910
- for aux_name, aux_value in data.items():
911
- if aux_value is None:
912
- continue
913
- categories.append(f"aux/{aux_name}")
914
- else:
915
- categories.append(str(slot_name))
916
-
917
- if categories:
918
- snapshot_types[str(output_type)] = sorted(set(categories))
919
-
920
- if not snapshot_types:
921
- return None
922
-
923
- return {
924
- "epoch": epoch,
925
- "step": step,
926
- "types": snapshot_types,
927
- }
928
-
929
930
  def _build_finish_patch(self) -> dict[str, Any]:
930
931
  patch = {
931
932
  "end_time": self._meta.get("end_time"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-train"
7
- version = "2.5.0"
7
+ version = "2.6.0"
8
8
  description = "Lightweight file-based experiment logger for PyTorch"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = ["numpy", "euler-dataset-contract"]
@@ -1488,6 +1488,162 @@ class TestSaveOutputsVariants:
1488
1488
  run.finish()
1489
1489
 
1490
1490
 
1491
+ # ═══════════════════════════════════════════════════════════════════════════
1492
+ # Output manifest
1493
+ # ═══════════════════════════════════════════════════════════════════════════
1494
+
1495
+ class TestSaveOutputsManifest:
1496
+ def test_manifest_written_for_basic_save(self, tmp_path):
1497
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1498
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1499
+ run.save_outputs(epoch=1, step=500, rgb=dict(pred=arr))
1500
+
1501
+ m = _read_json(run.dir / "outputs" / "epoch_1_step_500" / "manifest.json")
1502
+ assert m["version"] == 1
1503
+ assert m["epoch"] == 1
1504
+ assert m["step"] == 500
1505
+ assert "rgb" in m["output_types"]
1506
+ assert "pred" in m["output_types"]["rgb"]
1507
+ run.finish()
1508
+
1509
+ def test_manifest_indexed_files(self, tmp_path):
1510
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1511
+ imgs = [np.zeros((4, 4, 3), dtype=np.uint8) for _ in range(3)]
1512
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred=imgs))
1513
+
1514
+ slot = _read_json(
1515
+ run.dir / "outputs" / "epoch_0_step_0" / "manifest.json",
1516
+ )["output_types"]["rgb"]["pred"]
1517
+ assert slot["id_mode"] == "indexed"
1518
+ assert len(slot["files"]) == 3
1519
+ assert slot["files"][0] == {"sample_id": 0, "filename": "0000.png", "format": "png"}
1520
+ assert slot["files"][2]["sample_id"] == 2
1521
+ run.finish()
1522
+
1523
+ def test_manifest_named_files(self, tmp_path):
1524
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1525
+ a = np.zeros((4, 4, 3), dtype=np.uint8)
1526
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred={"scene_a": a, "scene_b": a}))
1527
+
1528
+ slot = _read_json(
1529
+ run.dir / "outputs" / "epoch_0_step_0" / "manifest.json",
1530
+ )["output_types"]["rgb"]["pred"]
1531
+ assert slot["id_mode"] == "named"
1532
+ ids = [f["sample_id"] for f in slot["files"]]
1533
+ assert "scene_a" in ids
1534
+ assert "scene_b" in ids
1535
+ run.finish()
1536
+
1537
+ def test_manifest_aux_slots(self, tmp_path):
1538
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1539
+ arr = np.ones((4, 4), dtype=np.float32)
1540
+ run.save_outputs(epoch=0, step=0, depth=dict(aux=dict(transmission=arr)))
1541
+
1542
+ m = _read_json(run.dir / "outputs" / "epoch_0_step_0" / "manifest.json")
1543
+ assert "aux/transmission" in m["output_types"]["depth"]
1544
+ run.finish()
1545
+
1546
+ def test_manifest_none_slots_excluded(self, tmp_path):
1547
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1548
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1549
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred=arr, gt=None))
1550
+
1551
+ slots = _read_json(
1552
+ run.dir / "outputs" / "epoch_0_step_0" / "manifest.json",
1553
+ )["output_types"]["rgb"]
1554
+ assert "pred" in slots
1555
+ assert "gt" not in slots
1556
+ run.finish()
1557
+
1558
+ def test_manifest_records_format_overrides(self, tmp_path):
1559
+ run = euler_train.init(
1560
+ dir=str(tmp_path / "r"), config={},
1561
+ output_formats={"depth": "npz"},
1562
+ )
1563
+ arr = np.ones((4, 4), dtype=np.float32)
1564
+ run.save_outputs(epoch=0, step=0, depth=dict(pred=arr))
1565
+
1566
+ m = _read_json(run.dir / "outputs" / "epoch_0_step_0" / "manifest.json")
1567
+ assert m["format_overrides"] == {"depth": "npz"}
1568
+ assert m["output_types"]["depth"]["pred"]["files"][0]["format"] == "npz"
1569
+ run.finish()
1570
+
1571
+ def test_manifest_mixed_named_and_indexed(self, tmp_path):
1572
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1573
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1574
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred={"x": arr}, gt=arr))
1575
+
1576
+ slots = _read_json(
1577
+ run.dir / "outputs" / "epoch_0_step_0" / "manifest.json",
1578
+ )["output_types"]["rgb"]
1579
+ assert slots["pred"]["id_mode"] == "named"
1580
+ assert slots["gt"]["id_mode"] == "indexed"
1581
+ run.finish()
1582
+
1583
+ def test_manifest_metadata(self, tmp_path):
1584
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1585
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1586
+ run.save_outputs(
1587
+ epoch=0, step=0,
1588
+ metadata={"dataset": "vkitti2", "split": "val"},
1589
+ rgb=dict(pred=arr),
1590
+ )
1591
+
1592
+ m = _read_json(run.dir / "outputs" / "epoch_0_step_0" / "manifest.json")
1593
+ assert m["metadata"] == {"dataset": "vkitti2", "split": "val"}
1594
+ run.finish()
1595
+
1596
+ def test_manifest_metadata_absent_when_not_provided(self, tmp_path):
1597
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1598
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1599
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred=arr))
1600
+
1601
+ m = _read_json(run.dir / "outputs" / "epoch_0_step_0" / "manifest.json")
1602
+ assert "metadata" not in m
1603
+ run.finish()
1604
+
1605
+ def test_manifest_metadata_requires_dataset(self, tmp_path):
1606
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1607
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1608
+ with pytest.raises(ValueError, match="dataset"):
1609
+ run.save_outputs(
1610
+ epoch=0, step=0,
1611
+ metadata={"split": "val"},
1612
+ rgb=dict(pred=arr),
1613
+ )
1614
+ run.finish()
1615
+
1616
+ def test_manifest_metadata_rejects_empty_dataset(self, tmp_path):
1617
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1618
+ arr = np.zeros((4, 4, 3), dtype=np.uint8)
1619
+ with pytest.raises(ValueError, match="dataset"):
1620
+ run.save_outputs(
1621
+ epoch=0, step=0,
1622
+ metadata={"dataset": " "},
1623
+ rgb=dict(pred=arr),
1624
+ )
1625
+ run.finish()
1626
+
1627
+ def test_no_manifest_when_all_none(self, tmp_path):
1628
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1629
+ run.save_outputs(epoch=0, step=0, rgb=None, depth=None)
1630
+
1631
+ manifest_path = run.dir / "outputs" / "epoch_0_step_0" / "manifest.json"
1632
+ assert not manifest_path.exists()
1633
+ run.finish()
1634
+
1635
+ def test_manifest_multiple_output_types(self, tmp_path):
1636
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1637
+ img = np.zeros((4, 4, 3), dtype=np.uint8)
1638
+ dep = np.ones((4, 4), dtype=np.float32)
1639
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred=img), depth=dict(pred=dep))
1640
+
1641
+ m = _read_json(run.dir / "outputs" / "epoch_0_step_0" / "manifest.json")
1642
+ assert m["output_types"]["rgb"]["pred"]["files"][0]["format"] == "png"
1643
+ assert m["output_types"]["depth"]["pred"]["files"][0]["format"] == "npy"
1644
+ run.finish()
1645
+
1646
+
1491
1647
  # ═══════════════════════════════════════════════════════════════════════════
1492
1648
  # init_checkpoint_dir
1493
1649
  # ═══════════════════════════════════════════════════════════════════════════
@@ -1766,14 +1922,13 @@ class TestStreaming:
1766
1922
  assert "elapsed_sec" not in val_event["records"][0]
1767
1923
 
1768
1924
  snapshot_event = consumer.events[5]
1769
- assert snapshot_event["snapshot"] == {
1770
- "epoch": 0,
1771
- "step": 1,
1772
- "types": {
1773
- "depth": ["aux/transmission"],
1774
- "rgb": ["gt", "pred"],
1775
- },
1776
- }
1925
+ snapshot = snapshot_event["snapshot"]
1926
+ assert snapshot["version"] == 1
1927
+ assert snapshot["epoch"] == 0
1928
+ assert snapshot["step"] == 1
1929
+ assert set(snapshot["output_types"]) == {"rgb", "depth"}
1930
+ assert set(snapshot["output_types"]["rgb"]) == {"pred", "gt"}
1931
+ assert "aux/transmission" in snapshot["output_types"]["depth"]
1777
1932
 
1778
1933
  checkpoint_event = consumer.events[6]
1779
1934
  assert checkpoint_event["checkpoint"] == {
File without changes
File without changes