Skip to content

Commit cb54559

Browse files
committed
Merge branch 'codex/professional-hardening'
2 parents 9eebe90 + 38a3a9b commit cb54559

25 files changed

Lines changed: 988 additions & 447 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.venv/
2+
.wheel-venv/
3+
.sdist-venv/
24
.worktrees/
35
.tmp/
46
.tmp_venv_*/

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **Hide nodes in the viewer:** Option to exclude selected nodes from the interactive visualization so dense or branching tensor networks stay readable without changing the underlying graph data.
13+
- **Public diagnostics contract:** Added package-specific exceptions (`TensorNetworkVizError`, `VisualizationInputError`, `AxisConfigurationError`, `UnsupportedEngineError`, `TensorDataError`, `MissingOptionalDependencyError`) and a documented `tensor_network_viz` logger with a default `NullHandler`.
14+
15+
### Changed
16+
17+
- **Typing and controller structure:** Fixed the interactive tensor-inspector typing issue reported by `pyright`, extracted the linked tensor-inspector controller, and split tensor-element rendering/color-scaling helpers into a dedicated module to keep responsibilities narrower.
18+
- **Verification workflow docs:** README, guide, and contribution docs now document the `.venv`-first verification flow (`quality`, `tests`, `smoke`, `package`) used before release and CI troubleshooting.
1319

1420
## [1.5.2] — 2026-04-05
1521

CONTRIBUTING.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ With the project venv (Windows):
4949

5050
Add tests for new features or bug fixes. All tests must pass before opening a PR.
5151

52+
Preferred full verification flow from the project `.venv`:
53+
54+
```powershell
55+
.\.venv\Scripts\python scripts\verify.py quality
56+
.\.venv\Scripts\python scripts\verify.py tests
57+
.\.venv\Scripts\python scripts\verify.py smoke
58+
.\.venv\Scripts\python scripts\verify.py package
59+
```
60+
61+
On Linux/macOS with the venv activated:
62+
63+
```bash
64+
python scripts/verify.py quality
65+
python scripts/verify.py tests
66+
python scripts/verify.py smoke
67+
python scripts/verify.py package
68+
```
69+
5270
### Optional: focused smoke and performance runs
5371

5472
Pytest markers are available for the render-specific regression checks:
@@ -149,9 +167,18 @@ python scripts/verify.py
149167
- **Target:** Python 3.11+
150168
- **Ruff rules:** E, F, I, B, UP, C4, SIM
151169
- **Typing:** Use type hints on public functions and modules; the codebase is `py.typed`
170+
- **Exceptions:** prefer `tensor_network_viz.exceptions` classes at public boundaries instead of raw `ValueError` / `ImportError` when you are surfacing user-facing library errors
171+
- **Logging:** use the `tensor_network_viz` logger; never call `logging.basicConfig()` from library code
152172

153173
Run `python scripts/verify.py` before committing. If you prefer, you can still run `ruff`, `pyright`, and `pytest` individually.
154174

175+
When you finish a Python task locally, run:
176+
177+
```powershell
178+
.\.venv\Scripts\python -m ruff check . --fix
179+
.\.venv\Scripts\python -m ruff format .
180+
```
181+
155182
## Opening Useful Issues
156183

157184
Use the [issue tracker](https://github.com/DOKOS-TAYOS/Tensor-Network-Visualization/issues).
@@ -210,6 +237,8 @@ Before opening a pull request, confirm:
210237
- `ruff check .` and `ruff format .` pass
211238
- `pyright` passes
212239
- `pytest` passes
240+
- `scripts/verify.py smoke` passes
241+
- `scripts/verify.py package` passes
213242
- New code has type hints and tests where appropriate
214243
- Documentation and examples are updated if behavior changed
215244
- PR description explains the change and links related issues

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,29 @@ python -m pip install tensor-network-visualization
4545

4646
Base dependencies are only `numpy`, `matplotlib`, and `networkx`.
4747

48+
## Errors and Logging
49+
50+
Public entry points now raise package-specific exceptions while remaining compatible with the
51+
built-in exception families they refine:
52+
53+
- `VisualizationInputError`: unsupported or ambiguous network input.
54+
- `AxisConfigurationError`: incompatible `ax` / `view` combinations.
55+
- `UnsupportedEngineError`: unknown engine or backend name.
56+
- `TensorDataError`: unsupported or missing tensor data for `show_tensor_elements(...)`.
57+
- `MissingOptionalDependencyError`: missing optional backend dependency.
58+
59+
All of them inherit from `TensorNetworkVizError`.
60+
61+
The package logger name is `tensor_network_viz`. It installs a `logging.NullHandler()` by default,
62+
so importing the library does not change your application's logging configuration.
63+
64+
```python
65+
import logging
66+
67+
logging.basicConfig(level=logging.DEBUG)
68+
logging.getLogger("tensor_network_viz").setLevel(logging.DEBUG)
69+
```
70+
4871
### Optional extras
4972

5073
| Need | Install |
@@ -393,6 +416,7 @@ python examples/tensor_elements_demo.py
393416
| Hover tooltips do nothing | Use an interactive Matplotlib backend; hover is not useful for PNG-only runs. |
394417
| Big graphs are slow | Set `tensor_label_refinement="never"`, reduce `layout_iterations`, or pass `positions`. |
395418
| `Unsupported tensor network engine` | Install the matching extra or pass the correct backend object. |
419+
| `AxisConfigurationError` when passing `ax` | Use a 2D axis for `view="2d"`, a 3D axis for `view="3d"`, and only pass `ax` to `show_tensor_elements(...)` for a single tensor. |
396420
| `show_tensor_elements(...)` fails on TensorKrowch nodes | Materialize the node tensors first; shape-only nodes do not expose element values. |
397421
| `show_tensor_elements(...)` fails on manual `pair_tensor(...)` lists | Use an `EinsumTrace` with live tensors instead; manual trace steps only describe contractions. |
398422
| Blank / duplicate Jupyter figure | Assign `fig, ax = show_tensor_network(...)` instead of leaving the tuple as the last line. |
@@ -421,3 +445,12 @@ Run the project checks:
421445
```powershell
422446
.\.venv\Scripts\python scripts\verify.py
423447
```
448+
449+
Useful slices:
450+
451+
```powershell
452+
.\.venv\Scripts\python scripts\verify.py quality
453+
.\.venv\Scripts\python scripts\verify.py tests
454+
.\.venv\Scripts\python scripts\verify.py smoke
455+
.\.venv\Scripts\python scripts\verify.py package
456+
```

docs/guide.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,33 @@ show_tensor_elements(
6666
| `show_controls` | If `True`, add compact `group + mode` controls and, when several tensors are present, a tensor slider. |
6767
| `show` | If `True`, display the figure immediately. If `False`, just return `(fig, ax)`. |
6868

69+
## Errors and Diagnostics
70+
71+
The public API raises package-specific exceptions so callers can distinguish user-input problems
72+
from unrelated runtime failures without parsing error strings:
73+
74+
- `TensorNetworkVizError`: root class for package-specific failures.
75+
- `VisualizationInputError`: unsupported or ambiguous network input.
76+
- `AxisConfigurationError`: incompatible `ax`, `view`, or figure-control setup.
77+
- `UnsupportedEngineError`: unknown backend name.
78+
- `TensorDataError`: unsupported tensor values or collections for `show_tensor_elements(...)`.
79+
- `MissingOptionalDependencyError`: backend requested but its dependency is not installed.
80+
81+
These classes deliberately preserve compatibility with the built-in families they refine
82+
(`ValueError` or `ImportError`), so existing downstream handlers keep working.
83+
84+
For diagnostics, enable the package logger:
85+
86+
```python
87+
import logging
88+
89+
logging.basicConfig(level=logging.DEBUG)
90+
logging.getLogger("tensor_network_viz").setLevel(logging.DEBUG)
91+
```
92+
93+
The logger name is `tensor_network_viz`, and the library installs a `NullHandler`, so imports stay
94+
quiet unless your application opts in.
95+
6996
## `PlotConfig` in Practice
7097

7198
`PlotConfig` is where visual behavior lives.
@@ -318,6 +345,15 @@ enough.
318345
Manual trace steps describe contractions, not tensor values. Use `EinsumTrace` and keep the traced
319346
tensors alive until you render them.
320347

348+
### `AxisConfigurationError` appears immediately
349+
350+
This means the plotting surface and the requested behavior disagree. Typical cases:
351+
352+
- `show_tensor_network(..., view="3d", ax=<2D axis>)`
353+
- `show_tensor_network(..., view="2d", ax=<3D axis>)`
354+
- `show_tensor_elements(..., ax=...)` with more than one tensor selected
355+
- `show_tensor_elements(..., show_controls=True, ax=...)` on a figure that already has extra axes
356+
321357
### Jupyter shows duplicate output
322358

323359
Prefer:

scripts/verify.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import argparse
4+
import logging
45
import subprocess
56
import sys
67
from dataclasses import dataclass
@@ -15,6 +16,7 @@ class VerificationStep:
1516

1617

1718
VerificationGroup: TypeAlias = tuple[VerificationStep, ...]
19+
LOGGER = logging.getLogger("tensor_network_viz.verify")
1820

1921

2022
def _repo_root() -> Path:
@@ -65,6 +67,7 @@ def _format_command(command: tuple[str, ...]) -> str:
6567

6668

6769
def _run_step(step: VerificationStep, repo_root: Path) -> None:
70+
LOGGER.debug("Running verification step '%s' in %s.", step.label, repo_root)
6871
print(f"[verify] {step.label}")
6972
print(f"[verify] $ {_format_command(step.command)}")
7073
subprocess.run(step.command, cwd=repo_root, check=True)
@@ -88,6 +91,7 @@ def main(argv: list[str] | None = None) -> int:
8891
parser = _build_parser()
8992
args = parser.parse_args(argv)
9093
repo_root = _repo_root()
94+
LOGGER.debug("Starting verification mode='%s' in repo_root=%s.", args.mode, repo_root)
9195

9296
try:
9397
for step in _ordered_steps(args.mode):

src/tensor_network_viz/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@
55
from matplotlib.figure import Figure
66
from mpl_toolkits.mplot3d.axes3d import Axes3D
77

8+
from . import _logging as _package_logging
89
from ._core.graph_cache import clear_tensor_network_graph_cache
910
from .config import EngineName, PlotConfig, ViewName
11+
from .exceptions import (
12+
AxisConfigurationError,
13+
MissingOptionalDependencyError,
14+
TensorDataError,
15+
TensorDataTypeError,
16+
TensorNetworkVizError,
17+
UnsupportedEngineError,
18+
VisualizationInputError,
19+
VisualizationTypeError,
20+
)
1021

1122
if TYPE_CHECKING:
1223
from .contraction_viewer import ContractionViewer2D, ContractionViewer3D
@@ -87,14 +98,22 @@ def __dir__() -> list[str]:
8798

8899

89100
__all__ = [
101+
"AxisConfigurationError",
90102
"ContractionViewer2D",
91103
"ContractionViewer3D",
92104
"EngineName",
93105
"EinsumTrace",
106+
"MissingOptionalDependencyError",
94107
"PlotConfig",
108+
"TensorDataError",
109+
"TensorDataTypeError",
95110
"TensorElementsConfig",
111+
"TensorNetworkVizError",
96112
"TenPyTensorNetwork",
113+
"UnsupportedEngineError",
97114
"ViewName",
115+
"VisualizationInputError",
116+
"VisualizationTypeError",
98117
"clear_tensor_network_graph_cache",
99118
"einsum",
100119
"einsum_trace_step",
@@ -103,3 +122,5 @@ def __dir__() -> list[str]:
103122
"show_tensor_elements",
104123
"show_tensor_network",
105124
]
125+
126+
_ = _package_logging

src/tensor_network_viz/_core/graph_cache.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections.abc import Callable
1111
from typing import Any
1212

13+
from .._logging import package_logger
1314
from .graph import _GraphData
1415

1516
_graph_weak_cache: weakref.WeakKeyDictionary[Any, dict[int, _GraphData]] = (
@@ -77,20 +78,27 @@ def _get_or_build_graph(
7778
if bucket is not None:
7879
hit = bucket.get(b_id)
7980
if hit is not None:
81+
package_logger.debug("Graph cache hit via weak cache for builder_id=%s.", b_id)
8082
return hit
8183
except TypeError:
8284
pass
8385

8486
builtin_hit = _builtin_container_cache_get(network, builder_id=b_id)
8587
if builtin_hit is not None:
88+
package_logger.debug("Graph cache hit via builtin container cache for builder_id=%s.", b_id)
8689
return builtin_hit
8790

8891
attr_bucket = getattr(network, _CACHE_ATTR, None)
8992
if isinstance(attr_bucket, dict):
9093
hit = attr_bucket.get(b_id)
9194
if isinstance(hit, _GraphData):
95+
package_logger.debug(
96+
"Graph cache hit via object attribute cache for builder_id=%s.",
97+
b_id,
98+
)
9299
return hit
93100

101+
package_logger.debug("Graph cache miss for builder_id=%s; rebuilding graph.", b_id)
94102
graph = builder(network)
95103

96104
try:
@@ -101,6 +109,7 @@ def _get_or_build_graph(
101109
bucket[b_id] = graph
102110
except TypeError:
103111
if _builtin_container_cache_put(network, builder_id=b_id, graph=graph):
112+
package_logger.debug("Stored graph in builtin container cache for builder_id=%s.", b_id)
104113
return graph
105114
try:
106115
if not isinstance(attr_bucket, dict):
@@ -123,6 +132,7 @@ def clear_tensor_network_graph_cache(
123132
Call this after in-place edits to a tensor network so the next draw re-extracts the structure.
124133
"""
125134
b_id = id(builder) if builder is not None else None
135+
package_logger.debug("Clearing tensor network graph cache for builder_id=%s.", b_id)
126136
try:
127137
bucket = _graph_weak_cache.get(network)
128138
if bucket is not None:

src/tensor_network_viz/_core/renderer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from matplotlib.figure import Figure
1515
from mpl_toolkits.mplot3d.axes3d import Axes3D
1616

17+
from .._logging import package_logger
1718
from .._matplotlib_state import get_reserved_bottom
1819
from .._typing import PositionMapping, root_figure
1920
from ..config import PlotConfig
@@ -240,6 +241,9 @@ def _resolve_draw_scale(graph: _GraphData, positions: NodePositions) -> float:
240241
d_min = _min_contraction_edge_length(graph, positions)
241242
if d_min is not None:
242243
return _geometric_draw_scale(d_min)
244+
package_logger.debug(
245+
"Falling back to heuristic draw scale because no valid contraction-edge length was found."
246+
)
243247
return _heuristic_draw_scale(graph, positions)
244248

245249

@@ -268,6 +272,10 @@ def _resolve_draw_scale_and_bond_curve_pad(
268272
if d_min is not None
269273
else _heuristic_draw_scale(graph, positions)
270274
)
275+
if d_min is None:
276+
package_logger.debug(
277+
"Falling back to heuristic draw scale and curve padding for renderer graph."
278+
)
271279

272280
best_curve = 0.0
273281
for record in records:

0 commit comments

Comments
 (0)