oriented-det 0.1.0__py3-none-any.whl

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.
Files changed (115) hide show
  1. export/__init__.py +9 -0
  2. export/ort_runtime.py +67 -0
  3. export/postprocess.py +151 -0
  4. export/scripts/__init__.py +6 -0
  5. export/scripts/build_faster_rcnn_savedmodel.py +104 -0
  6. export/scripts/export_onnx.py +210 -0
  7. export/scripts/onnx_to_savedmodel.py +35 -0
  8. export/scripts/predict_savedmodel.py +94 -0
  9. export/scripts/save_predictions_tf.py +447 -0
  10. export/scripts/to_tflite.py +32 -0
  11. export/tests/test_export_onnx_optional.py +82 -0
  12. export/tests/test_export_wrappers.py +105 -0
  13. export/tests/test_faster_rcnn_export_parity.py +201 -0
  14. export/tests/test_ort_runtime.py +41 -0
  15. export/tf_serving_model.py +96 -0
  16. export/val_dataset.py +116 -0
  17. export/wrappers.py +161 -0
  18. oriented_det/__init__.py +77 -0
  19. oriented_det/cli/__init__.py +92 -0
  20. oriented_det/cli/train.py +18 -0
  21. oriented_det/configs/_base_/augmentation.json +21 -0
  22. oriented_det/configs/_base_/datasets/dota_le90.json +21 -0
  23. oriented_det/configs/_base_/fp16.json +5 -0
  24. oriented_det/configs/_base_/models/oriented_rcnn_r50.json +26 -0
  25. oriented_det/configs/_base_/models/rotated_faster_rcnn_r50.json +53 -0
  26. oriented_det/configs/_base_/models/rotated_retinanet_r50.json +30 -0
  27. oriented_det/configs/_base_/preprocessing.json +8 -0
  28. oriented_det/configs/_base_/schedules/1x.json +33 -0
  29. oriented_det/configs/config.schema.json +404 -0
  30. oriented_det/configs/oriented_rcnn/dota_le90_1x.json +121 -0
  31. oriented_det/configs/oriented_rcnn/dota_le90_3x.json +11 -0
  32. oriented_det/configs/rotated_faster_rcnn/dota_le90_1x.json +119 -0
  33. oriented_det/configs/rotated_faster_rcnn/dota_le90_3x.json +9 -0
  34. oriented_det/configs/rotated_retinanet/dota_le90_1x.json +107 -0
  35. oriented_det/configs/rotated_retinanet/dota_le90_3x.json +30 -0
  36. oriented_det/data/__init__.py +89 -0
  37. oriented_det/data/airbus_playground.py +611 -0
  38. oriented_det/data/dota.py +741 -0
  39. oriented_det/data/dota_classes.py +26 -0
  40. oriented_det/data/evaluation.py +648 -0
  41. oriented_det/data/flips.py +115 -0
  42. oriented_det/data/preprocessing.py +335 -0
  43. oriented_det/data/tiling.py +399 -0
  44. oriented_det/data/transforms.py +377 -0
  45. oriented_det/geometry/__init__.py +8 -0
  46. oriented_det/geometry/poly.py +127 -0
  47. oriented_det/geometry/qbox.py +63 -0
  48. oriented_det/geometry/rbox.py +250 -0
  49. oriented_det/geometry/transforms.py +266 -0
  50. oriented_det/models/__init__.py +36 -0
  51. oriented_det/models/backbones/__init__.py +11 -0
  52. oriented_det/models/backbones/resnet_fpn.py +79 -0
  53. oriented_det/models/backbones/utils.py +81 -0
  54. oriented_det/models/bbox_coder.py +355 -0
  55. oriented_det/models/faster_rcnn_inference.py +494 -0
  56. oriented_det/models/horizontal_roi_coder.py +155 -0
  57. oriented_det/models/oriented_rcnn.py +1256 -0
  58. oriented_det/models/oriented_roi.py +1664 -0
  59. oriented_det/models/oriented_rpn.py +2104 -0
  60. oriented_det/models/rotated_retinanet.py +1030 -0
  61. oriented_det/models/utils.py +590 -0
  62. oriented_det/ops/__init__.py +58 -0
  63. oriented_det/ops/gpu_ops.py +1109 -0
  64. oriented_det/ops/iou.py +172 -0
  65. oriented_det/ops/kfiou.py +275 -0
  66. oriented_det/ops/nms.py +202 -0
  67. oriented_det/ops/probiou.py +165 -0
  68. oriented_det/ops/rotated_ops.py +122 -0
  69. oriented_det/ops/utils.py +257 -0
  70. oriented_det/pretrained/__init__.py +23 -0
  71. oriented_det/pretrained/hub.py +249 -0
  72. oriented_det/pretrained/manifest.json +46 -0
  73. oriented_det/runtime/__init__.py +29 -0
  74. oriented_det/runtime/checkpoint.py +274 -0
  75. oriented_det/runtime/collate.py +348 -0
  76. oriented_det/runtime/inference.py +1286 -0
  77. oriented_det/train/__init__.py +102 -0
  78. oriented_det/train/config.py +872 -0
  79. oriented_det/train/engine.py +2933 -0
  80. oriented_det/train/grouped_ce.py +139 -0
  81. oriented_det/train/piecewise_schedule.py +33 -0
  82. oriented_det/train/profiler.py +287 -0
  83. oriented_det/train/utils.py +999 -0
  84. oriented_det/utils/__init__.py +31 -0
  85. oriented_det/utils/config.py +376 -0
  86. oriented_det/utils/device.py +35 -0
  87. oriented_det/utils/logging.py +163 -0
  88. oriented_det/utils/progress.py +62 -0
  89. oriented_det/utils/viz.py +181 -0
  90. oriented_det-0.1.0.dist-info/METADATA +313 -0
  91. oriented_det-0.1.0.dist-info/RECORD +115 -0
  92. oriented_det-0.1.0.dist-info/WHEEL +5 -0
  93. oriented_det-0.1.0.dist-info/entry_points.txt +2 -0
  94. oriented_det-0.1.0.dist-info/licenses/LICENSE +202 -0
  95. oriented_det-0.1.0.dist-info/top_level.txt +3 -0
  96. tools/__init__.py +1 -0
  97. tools/app.py +1284 -0
  98. tools/dataset_stats.py +389 -0
  99. tools/dota_labels_to_comma.py +132 -0
  100. tools/free_gpu.py +142 -0
  101. tools/generate_airbus_playground_csv.py +154 -0
  102. tools/image_demo.py +200 -0
  103. tools/lr_finder.py +771 -0
  104. tools/measure_sampled_riou_error.py +483 -0
  105. tools/playground_to_dota.py +290 -0
  106. tools/pretrained_download.py +49 -0
  107. tools/preview_augmentation.py +510 -0
  108. tools/publish_checkpoint.py +96 -0
  109. tools/save_predictions.py +2350 -0
  110. tools/sync_vendored_configs.py +94 -0
  111. tools/tile_dota.py +447 -0
  112. tools/train.py +2324 -0
  113. tools/train_example.py +244 -0
  114. tools/train_multi_gpu.py +367 -0
  115. tools/visualize_boxes.py +183 -0
@@ -0,0 +1,31 @@
1
+ """Utility helpers for configuration management and visualization."""
2
+
3
+ from .config import FrozenConfig, load_config, merge_dicts, apply_overrides
4
+ from . import viz
5
+ from .device import get_device, is_gpu_device
6
+ from .logging import (
7
+ TraceLogger,
8
+ logger,
9
+ trace_function,
10
+ enable_tracing,
11
+ disable_tracing,
12
+ is_tracing_enabled,
13
+ )
14
+ from .progress import tqdm_progress_stream
15
+
16
+ __all__ = [
17
+ "FrozenConfig",
18
+ "load_config",
19
+ "merge_dicts",
20
+ "apply_overrides",
21
+ "get_device",
22
+ "is_gpu_device",
23
+ "viz",
24
+ "TraceLogger",
25
+ "logger",
26
+ "trace_function",
27
+ "enable_tracing",
28
+ "disable_tracing",
29
+ "is_tracing_enabled",
30
+ "tqdm_progress_stream",
31
+ ]
@@ -0,0 +1,376 @@
1
+ """Lightweight configuration helpers with strong validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, MutableMapping
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Iterable, Iterator, MutableSequence, Sequence
9
+ import ast
10
+ import json
11
+ import os
12
+
13
+ try: # Optional dependency for YAML configs.
14
+ import yaml # type: ignore
15
+ except Exception: # pragma: no cover - exercised when PyYAML is absent.
16
+ yaml = None # type: ignore
17
+
18
+ # Keys starting with this prefix are ignored when loading configs.
19
+ # Use for keeping alternative values visible without affecting behavior, e.g.:
20
+ # "learning_rate": 0.01,
21
+ # "_muted_learning_rate": 0.005 # ignored
22
+ MUTED_KEY_PREFIX = "_muted_"
23
+
24
+
25
+ def _ensure_mapping(value: Any) -> Dict[str, Any]:
26
+ if isinstance(value, Mapping):
27
+ return dict(value)
28
+ raise TypeError("Configuration root must be a mapping.")
29
+
30
+
31
+ def _deep_freeze(node: Any) -> Any:
32
+ if isinstance(node, Mapping):
33
+ return FrozenConfig({k: _deep_freeze(v) for k, v in node.items()})
34
+ if isinstance(node, list):
35
+ return tuple(_deep_freeze(v) for v in node)
36
+ return node
37
+
38
+
39
+ def _deep_clone(node: Any) -> Any:
40
+ if isinstance(node, Mapping):
41
+ return {k: _deep_clone(v) for k, v in node.items()}
42
+ if isinstance(node, list):
43
+ return [_deep_clone(v) for v in node]
44
+ return node
45
+
46
+
47
+ def _set_by_path(target: MutableMapping[str, Any], dotted_key: str, value: Any) -> None:
48
+ parts = dotted_key.split(".")
49
+ cursor: MutableMapping[str, Any] = target
50
+ for key in parts[:-1]:
51
+ if key not in cursor or not isinstance(cursor[key], MutableMapping):
52
+ cursor[key] = {}
53
+ cursor = cursor[key] # type: ignore[assignment]
54
+ cursor[parts[-1]] = value
55
+
56
+
57
+ def _parse_scalar(value: str) -> Any:
58
+ try:
59
+ return ast.literal_eval(value)
60
+ except Exception:
61
+ return value
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class FrozenConfig(Mapping[str, Any]):
66
+ """Immutable configuration wrapper with dotted-key access."""
67
+
68
+ _data: Mapping[str, Any]
69
+
70
+ def __getitem__(self, key: str) -> Any:
71
+ return self._data[key]
72
+
73
+ def __iter__(self) -> Iterator[str]:
74
+ return iter(self._data)
75
+
76
+ def __len__(self) -> int:
77
+ return len(self._data)
78
+
79
+ def get(self, dotted_key: str, default: Any = None) -> Any:
80
+ cursor: Any = self
81
+ for part in dotted_key.split("."):
82
+ if isinstance(cursor, Mapping) and part in cursor:
83
+ cursor = cursor[part]
84
+ else:
85
+ return default
86
+ return cursor
87
+
88
+ def to_dict(self) -> Dict[str, Any]:
89
+ def thaw(value: Any) -> Any:
90
+ if isinstance(value, FrozenConfig):
91
+ return {k: thaw(v) for k, v in value.items()}
92
+ if isinstance(value, tuple):
93
+ return [thaw(v) for v in value]
94
+ return value
95
+
96
+ return {k: thaw(v) for k, v in self.items()}
97
+
98
+
99
+ def merge_dicts(base: Mapping[str, Any], override: Mapping[str, Any]) -> Dict[str, Any]:
100
+ result = _deep_clone(base)
101
+ for key, value in override.items():
102
+ if key in result and isinstance(result[key], Mapping) and isinstance(value, Mapping):
103
+ result[key] = merge_dicts(result[key], value)
104
+ else:
105
+ result[key] = _deep_clone(value)
106
+ return result
107
+
108
+
109
+ def apply_overrides(cfg: Mapping[str, Any], overrides: Sequence[str] | Mapping[str, Any]) -> Dict[str, Any]:
110
+ result = _deep_clone(cfg)
111
+ if isinstance(overrides, Mapping):
112
+ for key, value in overrides.items():
113
+ _set_by_path(result, key, value)
114
+ else:
115
+ for override in overrides:
116
+ if "=" not in override:
117
+ raise ValueError(f"Override '{override}' must contain '='.")
118
+ key, value = override.split("=", 1)
119
+ _set_by_path(result, key, _parse_scalar(value))
120
+ return result
121
+
122
+
123
+ def _load_from_path(path: Path) -> Dict[str, Any]:
124
+ with path.open("r", encoding="utf-8") as handle:
125
+ if path.suffix in {".yaml", ".yml"}:
126
+ if yaml is None:
127
+ raise RuntimeError("PyYAML is required to load YAML configs.")
128
+ data = yaml.safe_load(handle)
129
+ else:
130
+ data = json.load(handle)
131
+ return _ensure_mapping(data)
132
+
133
+
134
+ # _base_ path prefix: paths are relative to oriented-det repository root (ORIENTED_DET_ROOT).
135
+ ODET_BASE_PREFIX = "@odet:"
136
+
137
+
138
+ def _oriented_det_root() -> Path:
139
+ """oriented-det repository root (``ORIENTED_DET_ROOT``)."""
140
+ raw = os.environ.get("ORIENTED_DET_ROOT")
141
+ if raw:
142
+ return Path(raw).expanduser().resolve()
143
+ try:
144
+ import oriented_det # noqa: WPS433 — runtime package location
145
+ except ImportError as exc:
146
+ raise FileNotFoundError(
147
+ "Cannot resolve @odet: base — set ORIENTED_DET_ROOT or install oriented_det."
148
+ ) from exc
149
+ return Path(oriented_det.__file__).resolve().parent.parent
150
+
151
+
152
+ def _framework_config_roots() -> list[Path]:
153
+ """Framework config directories to try for ``_base_`` fallbacks (in order).
154
+
155
+ Resolution order:
156
+ - ``ORIENTED_DET_CONFIG_ROOT`` (explicit override, sole entry when set)
157
+ - Editable checkout: ``<repo>/configs`` (full tree; preferred over vendored subset)
158
+ - Vendored wheel subset: ``oriented_det/configs``
159
+ """
160
+ roots: list[Path] = []
161
+ seen: set[Path] = set()
162
+
163
+ def _add(root: Path | None) -> None:
164
+ if root is None:
165
+ return
166
+ resolved = root.expanduser().resolve()
167
+ if resolved.is_dir() and resolved not in seen:
168
+ seen.add(resolved)
169
+ roots.append(resolved)
170
+
171
+ env = os.environ.get("ORIENTED_DET_CONFIG_ROOT")
172
+ if env:
173
+ _add(Path(env))
174
+ return roots
175
+
176
+ try:
177
+ import oriented_det
178
+
179
+ pkg = Path(oriented_det.__file__).resolve().parent
180
+ repo_configs = pkg.parent / "configs"
181
+ vendored = pkg / "configs"
182
+ # Repo tree is authoritative in editable installs; vendored may omit schedules like 6x.
183
+ if repo_configs.is_dir() and repo_configs != vendored:
184
+ _add(repo_configs)
185
+ _add(vendored)
186
+ except Exception:
187
+ pass
188
+ return roots
189
+
190
+
191
+ def _framework_config_root() -> Path | None:
192
+ """Primary framework config tree (first entry from :func:`_framework_config_roots`)."""
193
+ roots = _framework_config_roots()
194
+ return roots[0] if roots else None
195
+
196
+
197
+ def _normalize_base_ref_for_framework(base_path: str) -> str:
198
+ """Strip leading ``../`` so ``../_base_/models/x.json`` -> ``_base_/models/x.json``."""
199
+ ref = base_path.replace("\\", "/").lstrip("/")
200
+ while ref.startswith("../"):
201
+ ref = ref[3:]
202
+ if ref.startswith("./"):
203
+ ref = ref[2:]
204
+ return ref
205
+
206
+
207
+ def _resolve_base_path(base_path: str, current_file: Path) -> Path:
208
+ """Resolve a _base_ reference: @odet:, absolute, file-relative, or framework fallback.
209
+
210
+ External projects can keep dataset bases locally and inherit
211
+ model/schedule bases from the framework ``configs/_base_/`` tree when absent.
212
+ """
213
+ if base_path.startswith(ODET_BASE_PREFIX):
214
+ rel = base_path[len(ODET_BASE_PREFIX) :].lstrip("/")
215
+ return (_oriented_det_root() / rel).resolve()
216
+
217
+ base_path_obj = Path(base_path)
218
+ if base_path_obj.is_absolute():
219
+ if not base_path_obj.exists():
220
+ raise FileNotFoundError(f"Config file not found: {base_path_obj}")
221
+ return base_path_obj
222
+
223
+ local = (current_file.parent / base_path_obj).resolve()
224
+ if local.exists():
225
+ return local
226
+
227
+ rel = _normalize_base_ref_for_framework(base_path)
228
+ checked: list[Path] = []
229
+ for fw_root in _framework_config_roots():
230
+ fallback = (fw_root / rel).resolve()
231
+ checked.append(fallback)
232
+ if fallback.exists():
233
+ return fallback
234
+
235
+ raise FileNotFoundError(
236
+ f"Config file not found: {local}"
237
+ + (f" (also checked framework: {', '.join(str(p) for p in checked)})" if checked else "")
238
+ )
239
+
240
+
241
+ def _strip_muted_keys(cfg: Dict[str, Any], prefix: str = MUTED_KEY_PREFIX) -> None:
242
+ """Remove keys starting with prefix recursively. Modifies cfg in place."""
243
+ keys_to_remove = [k for k in cfg if isinstance(k, str) and k.startswith(prefix)]
244
+ for k in keys_to_remove:
245
+ del cfg[k]
246
+ for v in cfg.values():
247
+ if isinstance(v, dict):
248
+ _strip_muted_keys(v, prefix)
249
+
250
+
251
+ def _delete_keys(cfg: Dict[str, Any], keys_to_delete: Dict[str, Any]) -> None:
252
+ """Delete keys specified in keys_to_delete from cfg.
253
+
254
+ MMRotate-style deletion: {"key": {"_delete_": True}} removes "key" from parent.
255
+ """
256
+ for key, value in keys_to_delete.items():
257
+ if isinstance(value, dict) and value.get("_delete_") is True:
258
+ if key in cfg:
259
+ del cfg[key]
260
+ elif isinstance(value, dict) and isinstance(cfg.get(key), dict):
261
+ _delete_keys(cfg[key], value)
262
+
263
+
264
+ def _load_config_with_base(
265
+ path: Path,
266
+ *,
267
+ visited: set[Path] | None = None,
268
+ ) -> Dict[str, Any]:
269
+ """Load config file and recursively resolve _base_ inheritance.
270
+
271
+ Args:
272
+ path: Path to config file
273
+ visited: Set of already visited paths to detect circular dependencies
274
+
275
+ Returns:
276
+ Merged configuration dictionary with _base_ resolved
277
+ """
278
+ if visited is None:
279
+ visited = set()
280
+
281
+ path = path.resolve()
282
+
283
+ # Detect circular dependencies
284
+ if path in visited:
285
+ raise ValueError(f"Circular dependency detected: {path} is already being loaded")
286
+ visited.add(path)
287
+
288
+ if not path.exists():
289
+ raise FileNotFoundError(f"Config file not found: {path}")
290
+
291
+ cfg_dict = _load_from_path(path)
292
+
293
+ # Handle _base_ inheritance
294
+ if "_base_" in cfg_dict:
295
+ base_refs = cfg_dict.pop("_base_")
296
+
297
+ # Normalize to list
298
+ if isinstance(base_refs, str):
299
+ base_refs = [base_refs]
300
+ elif not isinstance(base_refs, list):
301
+ raise TypeError(f"_base_ must be a string or list of strings, got {type(base_refs)}")
302
+
303
+ # Load and merge base configs in order
304
+ merged = {}
305
+ for base_ref in base_refs:
306
+ if not isinstance(base_ref, str):
307
+ raise TypeError(f"Base reference must be a string, got {type(base_ref)}")
308
+ base_path = _resolve_base_path(base_ref, path)
309
+ base_cfg = _load_config_with_base(base_path, visited=visited)
310
+ merged = merge_dicts(merged, base_cfg)
311
+
312
+ # Merge current config over base configs
313
+ merged = merge_dicts(merged, cfg_dict)
314
+ cfg_dict = merged
315
+
316
+ visited.remove(path)
317
+ return cfg_dict
318
+
319
+
320
+ def load_config(
321
+ source: str | os.PathLike[str] | Mapping[str, Any],
322
+ *,
323
+ overrides: Sequence[str] | Mapping[str, Any] | None = None,
324
+ ) -> FrozenConfig:
325
+ """Load a configuration file or dict and apply optional overrides.
326
+
327
+ Supports MMRotate-style nested config inheritance via _base_ field.
328
+
329
+ Args:
330
+ source: Path to config file or dict
331
+ overrides: Optional overrides to apply after loading
332
+
333
+ Returns:
334
+ FrozenConfig instance with merged configuration
335
+
336
+ Examples:
337
+ >>> # Simple config
338
+ >>> cfg = load_config("config.json")
339
+
340
+ >>> # Config with base inheritance
341
+ >>> # config.json: {"_base_": "base.json", "lr": 0.01}
342
+ >>> cfg = load_config("config.json")
343
+
344
+ >>> # With overrides
345
+ >>> cfg = load_config("config.json", overrides=["training.lr=0.001"])
346
+ """
347
+ if isinstance(source, Mapping):
348
+ cfg_dict = _deep_clone(_ensure_mapping(source))
349
+ _strip_muted_keys(cfg_dict)
350
+ else:
351
+ path = Path(source)
352
+ if not path.exists():
353
+ raise FileNotFoundError(path)
354
+ # Load with base inheritance support
355
+ cfg_dict = _load_config_with_base(path)
356
+
357
+ # Strip _muted_* keys (keeps alternative values visible without affecting behavior)
358
+ _strip_muted_keys(cfg_dict)
359
+
360
+ # Handle deletion syntax (must be done before overrides)
361
+ _delete_keys(cfg_dict, cfg_dict)
362
+
363
+ if overrides:
364
+ cfg_dict = apply_overrides(cfg_dict, overrides)
365
+
366
+ return _deep_freeze(cfg_dict)
367
+
368
+
369
+ __all__ = [
370
+ "ODET_BASE_PREFIX",
371
+ "FrozenConfig",
372
+ "MUTED_KEY_PREFIX",
373
+ "load_config",
374
+ "merge_dicts",
375
+ "apply_overrides",
376
+ ]
@@ -0,0 +1,35 @@
1
+ """Device selection helpers for CPU, CUDA, and MPS (Apple Silicon)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ import torch
7
+ except ImportError:
8
+ torch = None # type: ignore
9
+
10
+
11
+ def get_device(prefer_cuda: bool = True) -> "torch.device":
12
+ """Return the best available device: CUDA > MPS (Apple Silicon) > CPU.
13
+
14
+ Use this when you want training or inference to use GPU when possible,
15
+ including on macOS with M1/M2/M3 (MPS).
16
+ """
17
+ if torch is None:
18
+ raise RuntimeError("PyTorch is required for device selection.")
19
+ if prefer_cuda and torch.cuda.is_available():
20
+ return torch.device("cuda")
21
+ if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
22
+ return torch.device("mps")
23
+ return torch.device("cpu")
24
+
25
+
26
+ def is_gpu_device(device: "torch.device") -> bool:
27
+ """Return True if the device is a GPU (CUDA or MPS)."""
28
+ if device is None:
29
+ return False
30
+ device_type = device.type if hasattr(device, "type") else str(device).split(":")[0]
31
+ if device_type == "cuda":
32
+ return torch is not None and torch.cuda.is_available()
33
+ if device_type == "mps":
34
+ return torch is not None and getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available()
35
+ return False
@@ -0,0 +1,163 @@
1
+ """Logging and tracing utilities for oriented-det framework.
2
+
3
+ This module provides configurable logging for debugging and performance analysis.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from contextlib import contextmanager
10
+ from typing import Optional, Callable, Any
11
+ import functools
12
+
13
+
14
+ class TraceLogger:
15
+ """Configurable logger for tracing function execution."""
16
+
17
+ _enabled: bool = False
18
+ _indent_level: int = 0
19
+ _indent_str: str = " "
20
+
21
+ @classmethod
22
+ def enable(cls) -> None:
23
+ """Enable tracing/logging."""
24
+ cls._enabled = True
25
+
26
+ @classmethod
27
+ def disable(cls) -> None:
28
+ """Disable tracing/logging."""
29
+ cls._enabled = False
30
+
31
+ @classmethod
32
+ def is_enabled(cls) -> bool:
33
+ """Check if tracing is enabled."""
34
+ return cls._enabled
35
+
36
+ @classmethod
37
+ def trace(cls, message: str, *args, **kwargs) -> None:
38
+ """Print a trace message if logging is enabled."""
39
+ if cls._enabled:
40
+ indent = cls._indent_str * cls._indent_level
41
+ formatted_msg = message.format(*args, **kwargs) if args or kwargs else message
42
+ print(f"{indent}{formatted_msg}")
43
+
44
+ @classmethod
45
+ @contextmanager
46
+ def trace_block(cls, message: str, *args, **kwargs):
47
+ """Context manager for tracing a code block with timing."""
48
+ if cls._enabled:
49
+ formatted_msg = message.format(*args, **kwargs) if args or kwargs else message
50
+ cls.trace(f"→ {formatted_msg}")
51
+ cls._indent_level += 1
52
+ start_time = time.time()
53
+ try:
54
+ yield
55
+ finally:
56
+ elapsed = time.time() - start_time
57
+ cls._indent_level -= 1
58
+ cls.trace(f"✅ {formatted_msg} ({elapsed:.3f}s)")
59
+ else:
60
+ yield
61
+
62
+ @classmethod
63
+ def trace_timing(cls, message: str, elapsed: float, *args, **kwargs) -> None:
64
+ """Trace a message with timing information."""
65
+ if cls._enabled:
66
+ formatted_msg = message.format(*args, **kwargs) if args or kwargs else message
67
+ cls.trace(f"{formatted_msg} ({elapsed:.3f}s)")
68
+
69
+ @classmethod
70
+ def trace_value(cls, name: str, value: Any, unit: Optional[str] = None) -> None:
71
+ """Trace a value with optional unit."""
72
+ if cls._enabled:
73
+ if unit:
74
+ cls.trace(f"{name}: {value} {unit}")
75
+ else:
76
+ cls.trace(f"{name}: {value}")
77
+
78
+
79
+ # Global instance
80
+ logger = TraceLogger()
81
+
82
+
83
+ def trace_function(func_name: Optional[str] = None):
84
+ """Decorator to trace function execution.
85
+
86
+ Args:
87
+ func_name: Optional custom name for the function in logs.
88
+ If None, uses the actual function name.
89
+
90
+ Example:
91
+ @trace_function()
92
+ def my_function(x, y):
93
+ return x + y
94
+
95
+ @trace_function("custom_name")
96
+ def another_function():
97
+ pass
98
+ """
99
+ def decorator(func: Callable) -> Callable:
100
+ name = func_name or func.__name__
101
+
102
+ @functools.wraps(func)
103
+ def wrapper(*args, **kwargs):
104
+ if logger.is_enabled():
105
+ # Format arguments for display
106
+ args_str = ", ".join([str(arg)[:50] for arg in args[:3]])
107
+ if len(args) > 3:
108
+ args_str += f", ... (+{len(args)-3} more)"
109
+ kwargs_str = ", ".join([f"{k}={str(v)[:30]}" for k, v in list(kwargs.items())[:2]])
110
+ if len(kwargs) > 2:
111
+ kwargs_str += f", ... (+{len(kwargs)-2} more)"
112
+
113
+ call_str = f"{name}({args_str}"
114
+ if kwargs_str:
115
+ call_str += f", {kwargs_str}"
116
+ call_str += ")"
117
+
118
+ logger.trace(f"→ {call_str}")
119
+ logger._indent_level += 1
120
+ start_time = time.time()
121
+ try:
122
+ result = func(*args, **kwargs)
123
+ elapsed = time.time() - start_time
124
+ logger._indent_level -= 1
125
+ logger.trace(f"✅ {name} returned ({elapsed:.3f}s)")
126
+ return result
127
+ except Exception as e:
128
+ elapsed = time.time() - start_time
129
+ logger._indent_level -= 1
130
+ logger.trace(f"❌ {name} raised {type(e).__name__}: {e} ({elapsed:.3f}s)")
131
+ raise
132
+ else:
133
+ return func(*args, **kwargs)
134
+
135
+ return wrapper
136
+ return decorator
137
+
138
+
139
+ # Convenience functions
140
+ def enable_tracing() -> None:
141
+ """Enable tracing globally."""
142
+ logger.enable()
143
+
144
+
145
+ def disable_tracing() -> None:
146
+ """Disable tracing globally."""
147
+ logger.disable()
148
+
149
+
150
+ def is_tracing_enabled() -> bool:
151
+ """Check if tracing is enabled."""
152
+ return logger.is_enabled()
153
+
154
+
155
+ __all__ = [
156
+ "TraceLogger",
157
+ "logger",
158
+ "trace_function",
159
+ "enable_tracing",
160
+ "disable_tracing",
161
+ "is_tracing_enabled",
162
+ ]
163
+
@@ -0,0 +1,62 @@
1
+ """Progress bar streams: show tqdm on the real console when stderr is piped (e.g. ``2>&1 | tee log``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import os
7
+ import sys
8
+ from typing import Any, Optional, TextIO
9
+
10
+ # Lazily opened /dev/tty (or Windows CON) for tqdm; closed at exit.
11
+ _tqdm_tty: Optional[TextIO] = None
12
+ _tqdm_tty_open_failed: bool = False
13
+ _devnull: Optional[TextIO] = None
14
+
15
+
16
+ def _close_tqdm_tty() -> None:
17
+ global _tqdm_tty
18
+ if _tqdm_tty is not None:
19
+ try:
20
+ _tqdm_tty.close()
21
+ except Exception:
22
+ pass
23
+ _tqdm_tty = None
24
+
25
+
26
+ def _get_devnull() -> TextIO:
27
+ global _devnull
28
+ if _devnull is None:
29
+ _devnull = open(os.devnull, "w")
30
+ return _devnull
31
+
32
+
33
+ def tqdm_progress_stream() -> Any:
34
+ """File object for ``tqdm(..., file=...)``, aligned with :func:`tools.train` / ``progress_stream`` on the training loop.
35
+
36
+ * If ``sys.stderr`` is a TTY, use it (normal interactive run).
37
+ * If not (shell redirect / ``| tee``), try ``/dev/tty`` (POSIX) or ``CON`` (Windows) so the bar
38
+ still updates on the user’s terminal while other output is captured in a file.
39
+ * If none of that works, use ``os.devnull`` so logs are not flooded with control characters.
40
+ """
41
+ global _tqdm_tty, _tqdm_tty_open_failed
42
+ if sys.stderr.isatty():
43
+ return sys.stderr
44
+ if _tqdm_tty is not None:
45
+ return _tqdm_tty
46
+ if not _tqdm_tty_open_failed:
47
+ if os.name == "posix":
48
+ try:
49
+ # Still works when e.g. ``python ... 2>&1 | tee x.log`` (stderr is a pipe, not a TTY).
50
+ _tqdm_tty = open("/dev/tty", "w", encoding="utf-8", buffering=1)
51
+ atexit.register(_close_tqdm_tty)
52
+ return _tqdm_tty
53
+ except OSError:
54
+ _tqdm_tty_open_failed = True
55
+ elif os.name == "nt":
56
+ try:
57
+ _tqdm_tty = open("CON", "w", encoding="utf-8", errors="replace", buffering=1)
58
+ atexit.register(_close_tqdm_tty)
59
+ return _tqdm_tty
60
+ except OSError:
61
+ _tqdm_tty_open_failed = True
62
+ return _get_devnull()