Source code for eclypse.io.assets

"""Serialisation helpers for ECLYPSE assets and asset spaces."""

from __future__ import annotations

from typing import (
    TYPE_CHECKING,
    Any,
)

from eclypse.graph.assets import (
    Additive,
    Concave,
    Convex,
    Multiplicative,
    Symbolic,
)
from eclypse.graph.assets.space import (
    AssetSpace,
    Choice,
    IntUniform,
    Sample,
    Uniform,
)
from eclypse.io._helpers import normalize_json_value

if TYPE_CHECKING:
    from eclypse.graph.assets import (
        Asset,
        AssetBucket,
    )

ASSET_TYPES: dict[str, type[Asset]] = {
    "additive": Additive,
    "multiplicative": Multiplicative,
    "concave": Concave,
    "convex": Convex,
    "symbolic": Symbolic,
}

ASSET_TYPE_NAMES = {asset_type: name for name, asset_type in ASSET_TYPES.items()}


[docs] def dump_asset_bucket(bucket: AssetBucket) -> dict[str, dict[str, Any]]: """Serialise an asset bucket. Args: bucket (AssetBucket): The asset bucket to serialise. Returns: dict[str, dict[str, Any]]: The serialised asset definitions. """ return {name: dump_asset(asset) for name, asset in bucket.items()}
[docs] def load_asset_bucket(data: dict[str, dict[str, Any]]) -> dict[str, Asset]: """Deserialise an asset bucket declaration. Args: data (dict[str, dict[str, Any]]): The serialised asset definitions. Returns: dict[str, Asset]: The deserialised asset mapping. """ return {name: load_asset(asset_data) for name, asset_data in data.items()}
[docs] def dump_asset(asset: Asset) -> dict[str, Any]: """Serialise an asset definition. Args: asset (Asset): The asset to serialise. Returns: dict[str, Any]: The serialised asset definition. Raises: ValueError: If the asset type or initialiser is not supported. """ try: asset_type = ASSET_TYPE_NAMES[type(asset)] except KeyError as exc: raise ValueError(f"Unsupported asset type: {type(asset).__name__}") from exc return { "type": asset_type, "lower_bound": normalize_json_value(asset.lower_bound), "upper_bound": normalize_json_value(asset.upper_bound), "functional": asset.functional, "init": dump_init(asset.init_fn), }
[docs] def load_asset(data: dict[str, Any]) -> Asset: """Deserialise an asset definition. Args: data (dict[str, Any]): The serialised asset definition. Returns: Asset: The deserialised asset. Raises: ValueError: If the asset type is unknown. """ asset_type_name = data["type"] try: asset_type = ASSET_TYPES[asset_type_name] except KeyError as exc: raise ValueError(f"Unknown asset type: {asset_type_name}") from exc return asset_type( data["lower_bound"], data["upper_bound"], load_init(data.get("init", {"type": "none"})), data.get("functional", True), )
[docs] def dump_init(init_fn: Any) -> dict[str, Any]: """Serialise an asset initialiser. Args: init_fn (Any): The asset initialiser. Returns: dict[str, Any]: The serialised initialiser. Raises: ValueError: If the initialiser cannot be represented portably. """ if init_fn is None: return {"type": "none"} if isinstance(init_fn, AssetSpace): return dump_space(init_fn) primitive = _primitive_from_init(init_fn) if primitive is not _MISSING: return {"type": "value", "value": normalize_json_value(primitive)} raise ValueError("Only primitive values and AssetSpace initialisers are supported.")
[docs] def load_init(data: dict[str, Any]) -> Any: """Deserialise an asset initialiser. Args: data (dict[str, Any]): The serialised initialiser. Returns: Any: The deserialised initialiser. Raises: ValueError: If the initialiser type is unknown. """ init_type = data["type"] if init_type == "none": return None if init_type == "value": return data["value"] return load_space(data)
[docs] def dump_space(space: AssetSpace) -> dict[str, Any]: """Serialise an asset space. Args: space (AssetSpace): The asset space to serialise. Returns: dict[str, Any]: The serialised asset space. Raises: ValueError: If the asset space type is unsupported. """ if isinstance(space, Choice): return {"type": "choice", "choices": normalize_json_value(space.choices)} if isinstance(space, Uniform): return {"type": "uniform", "low": space.low, "high": space.high} if isinstance(space, IntUniform): return { "type": "int_uniform", "low": space.low, "high": space.high, "step": space.step, } if isinstance(space, Sample): return { "type": "sample", "population": normalize_json_value(space.population), "k": normalize_json_value(space.k), "counts": normalize_json_value(space.counts), } raise ValueError(f"Unsupported asset space type: {type(space).__name__}")
[docs] def load_space(data: dict[str, Any]) -> AssetSpace: """Deserialise an asset space. Args: data (dict[str, Any]): The serialised asset space. Returns: AssetSpace: The deserialised asset space. Raises: ValueError: If the asset space type is unknown. """ space_type = data["type"] if space_type == "choice": return Choice(data["choices"]) if space_type == "uniform": return Uniform(data["low"], data["high"]) if space_type == "int_uniform": return IntUniform(data["low"], data["high"], data.get("step", 1)) if space_type == "sample": k = data["k"] if isinstance(k, list): k = tuple(k) return Sample(data["population"], k, data.get("counts")) raise ValueError(f"Unknown asset space type: {space_type}")
class _Missing: """Sentinel used when an initialiser cannot be decoded as a primitive.""" _MISSING = _Missing() def _primitive_from_init(init_fn: Any) -> Any: """Return the primitive captured by an Asset value initialiser, if any. Args: init_fn (Any): The initialiser function to inspect. Returns: Any: The captured primitive value, or an internal sentinel. """ closure = getattr(init_fn, "__closure__", None) if getattr(init_fn, "__name__", "") == "_tmp_init_fn" and closure: return closure[0].cell_contents return _MISSING