Source code for reporting.figure_cache
"""Content-derived identities for figures shared by documentation pages."""
import hashlib
import importlib.metadata
import json
import locale
from functools import lru_cache
from pathlib import Path
import numpy as np
from simtools.visualization import matplotlib_backend, visualize
from simtools.visualization.matplotlib_backend import BACKEND_NAME
[docs]
def figure_cache_key(renderer, renderer_modules, plot_config, rendered_data, context=None):
"""Return a SHA-256 key for identical plot inputs and rendering settings."""
renderer_modules = {
"figure_cache": __file__,
"matplotlib_backend": matplotlib_backend.__file__,
**renderer_modules,
}
renderer_source = {
name: _source_digest(Path(module_path).resolve())
for name, module_path in renderer_modules.items()
}
output_settings = {
key: visualize.figure_output_settings(context).get(key)
for key in ("figure_format", "figure_dpi", "figure_png_palette")
}
output_settings.update(
{
"locale": locale.setlocale(locale.LC_ALL),
"matplotlib_backend": BACKEND_NAME,
"matplotlib_version": importlib.metadata.version("matplotlib"),
}
)
payload = {
"cache_format": 1,
"renderer": renderer,
"renderer_source": renderer_source,
"plot_config": plot_config,
"rendered_data": rendered_data,
"output_settings": output_settings,
}
canonical_payload = json.dumps(
_canonicalize(payload), sort_keys=True, separators=(",", ":"), ensure_ascii=True
)
return hashlib.sha256(canonical_payload.encode("utf-8")).hexdigest()
@lru_cache(maxsize=8)
def _source_digest(path):
"""Return a SHA-256 digest for a renderer source module."""
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def _canonicalize(value):
"""Convert NumPy, dataclass and structured plot inputs into stable JSON values."""
if hasattr(value, "__dataclass_fields__"):
value = {field: getattr(value, field) for field in value.__dataclass_fields__}
if isinstance(value, dict):
return {str(key): _canonicalize(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [_canonicalize(item) for item in value]
if isinstance(value, np.ndarray):
return {
"dtype": value.dtype.descr if value.dtype.names else value.dtype.str,
"shape": list(value.shape),
"values": _canonicalize(value.tolist()),
}
if isinstance(value, np.generic):
value = value.item()
if isinstance(value, Path):
return value.as_posix()
if isinstance(value, float) and not np.isfinite(value):
return repr(value)
return value