euler-train 2.4.1__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.4.1
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,15 +70,30 @@ 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
- items = _unpack(data)
72
76
  visualization = _resolve_visualization(
73
77
  output_type, leaf_name, visualization_overrides,
74
78
  )
79
+
80
+ # Dict with string keys → named outputs (e.g. {"scene_042": img}).
81
+ if isinstance(data, dict):
82
+ files: list[dict[str, Any]] = []
83
+ for name, raw in data.items():
84
+ item = _prepare(raw)
85
+ fmt = _resolve_format(item, output_type, leaf_name, format_overrides)
86
+ _save_item(slot_dir / f"{name}.{fmt}", item, fmt, visualization)
87
+ files.append({"sample_id": name, "filename": f"{name}.{fmt}", "format": fmt})
88
+ return {"id_mode": "named", "files": files}
89
+
90
+ items = _unpack(data)
91
+ files = []
75
92
  for idx, item in enumerate(items):
76
93
  fmt = _resolve_format(item, output_type, leaf_name, format_overrides)
77
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}
78
97
 
79
98
 
80
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.
@@ -375,11 +376,34 @@ class Run:
375
376
  aux=dict(transmission=t_map)),
376
377
  )
377
378
 
379
+ Slot values can also be dicts mapping custom string IDs to
380
+ items, producing named files instead of sequential indices::
381
+
382
+ run.save_outputs(
383
+ epoch=1, step=500,
384
+ rgb=dict(pred={"scene_042": img_a, "scene_117": img_b}),
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
+
378
397
  Accepted slot keys: ``pred``, ``gt``, ``input``, ``aux``.
379
398
  ``aux`` expects a sub-dict of named arrays.
380
399
 
381
400
  Returns the directory that was written to.
382
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
+ )
383
407
  parts: list[str] = []
384
408
  if epoch is not None:
385
409
  parts.append(f"epoch_{epoch}")
@@ -388,21 +412,38 @@ class Run:
388
412
  dirname = "_".join(parts) or "unspecified"
389
413
  base = self.dir / "outputs" / dirname
390
414
 
415
+ manifest_types: dict[str, Any] = {}
391
416
  for output_type, slots in output_types.items():
392
417
  if slots is None:
393
418
  continue
394
- save_output_tree(
419
+ slot_manifest = save_output_tree(
395
420
  base / output_type,
396
421
  slots,
397
422
  self._output_formats,
398
423
  self._output_visualization,
399
424
  output_type,
400
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)
401
443
  if base.exists():
402
444
  self._record_artifact_update(base)
403
445
  self._flush_meta()
404
- snapshot = self._build_output_snapshot(epoch, step, output_types)
405
- if snapshot is not None:
446
+ if manifest_types and epoch is not None and step is not None:
406
447
  self._emit_stream_event(
407
448
  {
408
449
  "type": "output_snapshot",
@@ -411,7 +452,13 @@ class Run:
411
452
  epoch,
412
453
  step,
413
454
  ),
414
- "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
+ },
415
462
  },
416
463
  )
417
464
  return base
@@ -880,44 +927,6 @@ class Run:
880
927
  ]
881
928
  return tags or None
882
929
 
883
- def _build_output_snapshot(
884
- self,
885
- epoch: int | None,
886
- step: int | None,
887
- output_types: dict[str, Any],
888
- ) -> dict[str, Any] | None:
889
- if epoch is None or step is None:
890
- return None
891
-
892
- snapshot_types: dict[str, list[str]] = {}
893
- for output_type, slots in output_types.items():
894
- if slots is None or not isinstance(slots, Mapping):
895
- continue
896
-
897
- categories: list[str] = []
898
- for slot_name, data in slots.items():
899
- if data is None:
900
- continue
901
- if slot_name == "aux" and isinstance(data, Mapping):
902
- for aux_name, aux_value in data.items():
903
- if aux_value is None:
904
- continue
905
- categories.append(f"aux/{aux_name}")
906
- else:
907
- categories.append(str(slot_name))
908
-
909
- if categories:
910
- snapshot_types[str(output_type)] = sorted(set(categories))
911
-
912
- if not snapshot_types:
913
- return None
914
-
915
- return {
916
- "epoch": epoch,
917
- "step": step,
918
- "types": snapshot_types,
919
- }
920
-
921
930
  def _build_finish_patch(self) -> dict[str, Any]:
922
931
  patch = {
923
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.4.1
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.4.1"
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"]
@@ -1444,6 +1444,42 @@ class TestSaveOutputsVariants:
1444
1444
  assert path.name == "epoch_2_step_500"
1445
1445
  run.finish()
1446
1446
 
1447
+ def test_dict_slot_produces_named_files(self, tmp_path):
1448
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1449
+ a = np.zeros((4, 4, 3), dtype=np.uint8)
1450
+ b = np.ones((4, 4, 3), dtype=np.uint8) * 128
1451
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred={"scene_a": a, "scene_b": b}))
1452
+
1453
+ pred_dir = run.dir / "outputs" / "epoch_0_step_0" / "rgb" / "pred"
1454
+ names = sorted(f.name for f in pred_dir.iterdir())
1455
+ assert names == ["scene_a.png", "scene_b.png"]
1456
+ run.finish()
1457
+
1458
+ def test_dict_slot_in_aux(self, tmp_path):
1459
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1460
+ m1 = np.ones((4, 4), dtype=np.float32)
1461
+ m2 = np.zeros((4, 4), dtype=np.float32)
1462
+ run.save_outputs(
1463
+ epoch=0, step=0,
1464
+ depth=dict(aux=dict(attn={"head_0": m1, "head_1": m2})),
1465
+ )
1466
+
1467
+ attn_dir = run.dir / "outputs" / "epoch_0_step_0" / "depth" / "aux" / "attn"
1468
+ names = sorted(f.name for f in attn_dir.iterdir())
1469
+ assert names == ["head_0.npy", "head_1.npy"]
1470
+ run.finish()
1471
+
1472
+ def test_mixed_dict_and_array_slots(self, tmp_path):
1473
+ run = euler_train.init(dir=str(tmp_path / "r"), config={})
1474
+ a = np.zeros((4, 4, 3), dtype=np.uint8)
1475
+ run.save_outputs(epoch=0, step=0, rgb=dict(pred={"frame_x": a}, gt=a))
1476
+
1477
+ pred_dir = run.dir / "outputs" / "epoch_0_step_0" / "rgb" / "pred"
1478
+ gt_dir = run.dir / "outputs" / "epoch_0_step_0" / "rgb" / "gt"
1479
+ assert sorted(f.name for f in pred_dir.iterdir()) == ["frame_x.png"]
1480
+ assert sorted(f.name for f in gt_dir.iterdir()) == ["0000.png"]
1481
+ run.finish()
1482
+
1447
1483
  def test_returns_output_base_path(self, tmp_path):
1448
1484
  run = euler_train.init(dir=str(tmp_path / "r"), config={})
1449
1485
  arr = np.zeros((4, 4, 3), dtype=np.uint8)
@@ -1452,6 +1488,162 @@ class TestSaveOutputsVariants:
1452
1488
  run.finish()
1453
1489
 
1454
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
+
1455
1647
  # ═══════════════════════════════════════════════════════════════════════════
1456
1648
  # init_checkpoint_dir
1457
1649
  # ═══════════════════════════════════════════════════════════════════════════
@@ -1730,14 +1922,13 @@ class TestStreaming:
1730
1922
  assert "elapsed_sec" not in val_event["records"][0]
1731
1923
 
1732
1924
  snapshot_event = consumer.events[5]
1733
- assert snapshot_event["snapshot"] == {
1734
- "epoch": 0,
1735
- "step": 1,
1736
- "types": {
1737
- "depth": ["aux/transmission"],
1738
- "rgb": ["gt", "pred"],
1739
- },
1740
- }
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"]
1741
1932
 
1742
1933
  checkpoint_event = consumer.events[6]
1743
1934
  assert checkpoint_event["checkpoint"] == {
File without changes
File without changes