adaptcompile 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.
@@ -0,0 +1,30 @@
1
+ """Public API for adaptcompile."""
2
+
3
+ from importlib.metadata import version as _distribution_version
4
+
5
+ from .dataset import AdaptationDataset
6
+ from .episode import LearningEpisode
7
+ from .errors import AdaptCompileError, SerializationError, ValidationError
8
+ from .family import ProgramFamily
9
+ from .geometry import AdaptationGeometry
10
+ from .model import ModelContext
11
+ from .program import ProgramSpec
12
+ from .result import AdaptationResult
13
+ from .study import AdaptationStudy
14
+
15
+ __version__ = _distribution_version("adaptcompile")
16
+
17
+ __all__ = [
18
+ "AdaptCompileError",
19
+ "AdaptationDataset",
20
+ "AdaptationGeometry",
21
+ "AdaptationResult",
22
+ "AdaptationStudy",
23
+ "LearningEpisode",
24
+ "ModelContext",
25
+ "ProgramFamily",
26
+ "ProgramSpec",
27
+ "SerializationError",
28
+ "ValidationError",
29
+ "__version__",
30
+ ]
@@ -0,0 +1,46 @@
1
+ """Internal validation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from numbers import Real
8
+ from types import MappingProxyType
9
+ from typing import Any
10
+
11
+ from .errors import ValidationError
12
+
13
+
14
+ def nonempty_name(value: Any, *, field: str) -> str:
15
+ if not isinstance(value, str) or not value.strip():
16
+ raise ValidationError(f"{field} must be a non-empty string")
17
+ return value
18
+
19
+
20
+ def string_keyed_mapping(value: Any, *, field: str) -> Mapping[str, Any]:
21
+ if not isinstance(value, Mapping):
22
+ raise ValidationError(f"{field} must be a mapping")
23
+ copied: dict[str, Any] = {}
24
+ for key, item in value.items():
25
+ nonempty_name(key, field=f"{field} key")
26
+ copied[key] = item
27
+ return MappingProxyType(copied)
28
+
29
+
30
+ def score(value: Any, *, field: str) -> float:
31
+ if isinstance(value, bool) or not isinstance(value, Real):
32
+ raise ValidationError(f"{field} must be a real number")
33
+ converted = float(value)
34
+ if not math.isfinite(converted):
35
+ raise ValidationError(f"{field} must be finite")
36
+ return converted
37
+
38
+
39
+ def score_mapping(value: Any, *, field: str) -> Mapping[str, float]:
40
+ mapping = string_keyed_mapping(value, field=field)
41
+ return MappingProxyType(
42
+ {
43
+ name: score(item, field=f"{field}[{name!r}]")
44
+ for name, item in mapping.items()
45
+ }
46
+ )
@@ -0,0 +1,160 @@
1
+ """Corpus-level collections of observed adaptations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Sequence
6
+ from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
7
+
8
+ from .episode import EpisodeIdentityKey, _episode_identity_key
9
+ from .errors import ValidationError
10
+ from .result import AdaptationResult
11
+
12
+ if TYPE_CHECKING:
13
+ from pandas import DataFrame
14
+
15
+ ModelIdentityKey = tuple[str, str | None, str | None]
16
+ GroupKey = TypeVar("GroupKey", bound=Hashable)
17
+
18
+
19
+ class AdaptationDataset(Sequence[AdaptationResult]):
20
+ """An immutable corpus of observations spanning models, episodes, and programs.
21
+
22
+ Repeated ``(model, episode, program)`` observations are retained because seeds,
23
+ experiment runs, and measured outcomes may legitimately differ.
24
+ """
25
+
26
+ __slots__ = ("_results",)
27
+
28
+ def __init__(self, results: Iterable[AdaptationResult]) -> None:
29
+ collected = tuple(results)
30
+ if not collected:
31
+ raise ValidationError("an AdaptationDataset requires at least one result")
32
+ if any(not isinstance(result, AdaptationResult) for result in collected):
33
+ raise ValidationError(
34
+ "all dataset entries must be AdaptationResult instances"
35
+ )
36
+ self._results = collected
37
+
38
+ @property
39
+ def results(self) -> tuple[AdaptationResult, ...]:
40
+ return self._results
41
+
42
+ @overload
43
+ def __getitem__(self, index: int) -> AdaptationResult: ...
44
+
45
+ @overload
46
+ def __getitem__(self, index: slice) -> tuple[AdaptationResult, ...]: ...
47
+
48
+ def __getitem__(
49
+ self, index: int | slice
50
+ ) -> AdaptationResult | tuple[AdaptationResult, ...]:
51
+ return self._results[index]
52
+
53
+ def __len__(self) -> int:
54
+ return len(self._results)
55
+
56
+ def __iter__(self) -> Iterator[AdaptationResult]:
57
+ return iter(self._results)
58
+
59
+ def __repr__(self) -> str:
60
+ return f"AdaptationDataset(n_results={len(self)})"
61
+
62
+ def by_model(self) -> dict[ModelIdentityKey, AdaptationDataset]:
63
+ """Group by ``ModelContext.identity_key``."""
64
+ return self._grouped(lambda result: result.model_context.identity_key)
65
+
66
+ def by_episode(self) -> dict[EpisodeIdentityKey, AdaptationDataset]:
67
+ """Group by explicit ID or conservative serialized episode configuration."""
68
+ return self._grouped(lambda result: _episode_identity_key(result.episode))
69
+
70
+ def by_family(self) -> dict[str | None, AdaptationDataset]:
71
+ """Group by conceptual fingerprint; ``None`` retains familyless programs."""
72
+ return self._grouped(
73
+ lambda result: (
74
+ result.program.family.fingerprint
75
+ if result.program.family is not None
76
+ else None
77
+ )
78
+ )
79
+
80
+ def by_program(self) -> dict[str, AdaptationDataset]:
81
+ """Group by concrete executable program fingerprint."""
82
+ return self._grouped(lambda result: result.program.fingerprint)
83
+
84
+ def filter(
85
+ self, predicate: Callable[[AdaptationResult], bool]
86
+ ) -> AdaptationDataset:
87
+ """Return observations matching *predicate*.
88
+
89
+ Like direct construction, an empty selection raises :class:`ValidationError`.
90
+ """
91
+ return AdaptationDataset(result for result in self if predicate(result))
92
+
93
+ def to_records(self) -> list[dict[str, Any]]:
94
+ """Return unambiguous flat observation records with nested run metadata."""
95
+ records: list[dict[str, Any]] = []
96
+ for result in self:
97
+ family = result.program.family
98
+ record: dict[str, Any] = {
99
+ "model_id": result.model_context.model_id,
100
+ "model_revision": result.model_context.revision,
101
+ "base_state_id": result.model_context.base_state_id,
102
+ "episode": result.episode.name,
103
+ "episode_id": result.episode.episode_id,
104
+ "program_family_fingerprint": (
105
+ family.fingerprint if family is not None else None
106
+ ),
107
+ "program_family_name": family.name if family is not None else None,
108
+ "program_family_method": (
109
+ family.method if family is not None else None
110
+ ),
111
+ "program_family_parameters": (
112
+ family.to_dict()["parameters"] if family is not None else None
113
+ ),
114
+ "program_fingerprint": result.program.fingerprint,
115
+ "program_name": result.program.name,
116
+ "method": result.program.method,
117
+ "program_parameters": result.program.to_dict()["parameters"],
118
+ "metadata": result.to_dict()["metadata"],
119
+ }
120
+ record.update(
121
+ {
122
+ f"before_{name}": value
123
+ for name, value in result.before_geometry.items()
124
+ }
125
+ )
126
+ record.update(
127
+ {
128
+ f"after_{name}": value
129
+ for name, value in result.after_geometry.items()
130
+ }
131
+ )
132
+ record.update(
133
+ {
134
+ f"delta_{name}": value
135
+ for name, value in result.delta_geometry.items()
136
+ }
137
+ )
138
+ records.append(record)
139
+ return records
140
+
141
+ def to_dataframe(self) -> DataFrame:
142
+ """Return a pandas DataFrame (requires the ``dataframe`` extra)."""
143
+ try:
144
+ from pandas import DataFrame
145
+ except ImportError as error: # pragma: no cover - depends on environment
146
+ raise ImportError(
147
+ "to_dataframe() requires pandas; install adaptcompile[dataframe]"
148
+ ) from error
149
+ return cast("DataFrame", DataFrame(self.to_records()))
150
+
151
+ def _grouped(
152
+ self, key: Callable[[AdaptationResult], GroupKey]
153
+ ) -> dict[GroupKey, AdaptationDataset]:
154
+ grouped: dict[GroupKey, list[AdaptationResult]] = {}
155
+ for result in self:
156
+ grouped.setdefault(key(result), []).append(result)
157
+ return {
158
+ identity: AdaptationDataset(results)
159
+ for identity, results in grouped.items()
160
+ }
@@ -0,0 +1,110 @@
1
+ """Learning episode representation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from os import PathLike
8
+ from typing import Any
9
+
10
+ from ._validation import nonempty_name, string_keyed_mapping
11
+ from .errors import ValidationError
12
+ from .serialization import (
13
+ immutable_json_mapping,
14
+ json_dumps,
15
+ read_json,
16
+ to_json_safe,
17
+ write_json,
18
+ )
19
+
20
+ EpisodeIdentityKey = tuple[str, str]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class LearningEpisode:
25
+ """A model-independent learning problem and its evaluation sets.
26
+
27
+ Dataset-like objects are held by reference and deliberately excluded from
28
+ serialization. The evaluation mapping and metadata are detached and read-only.
29
+ """
30
+
31
+ name: str
32
+ train: Any
33
+ evaluations: Mapping[str, Any]
34
+ metadata: Mapping[str, Any] = field(default_factory=dict)
35
+ episode_id: str | None = None
36
+
37
+ def __post_init__(self) -> None:
38
+ object.__setattr__(self, "name", nonempty_name(self.name, field="name"))
39
+ evaluations = string_keyed_mapping(self.evaluations, field="evaluations")
40
+ if not evaluations:
41
+ raise ValidationError(
42
+ "evaluations must contain at least one evaluation set"
43
+ )
44
+ object.__setattr__(self, "evaluations", evaluations)
45
+ object.__setattr__(
46
+ self,
47
+ "metadata",
48
+ immutable_json_mapping(self.metadata, location="metadata"),
49
+ )
50
+ if self.episode_id is not None:
51
+ object.__setattr__(
52
+ self,
53
+ "episode_id",
54
+ nonempty_name(self.episode_id, field="episode_id"),
55
+ )
56
+
57
+ @property
58
+ def evaluation_names(self) -> tuple[str, ...]:
59
+ """Names of the declared evaluation sets, in construction order."""
60
+ return tuple(self.evaluations)
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ """Serialize configuration only; dataset contents are intentionally omitted."""
64
+ return {
65
+ "name": self.name,
66
+ "episode_id": self.episode_id,
67
+ "evaluation_names": list(self.evaluation_names),
68
+ "metadata": to_json_safe(self.metadata, location="metadata"),
69
+ }
70
+
71
+ @classmethod
72
+ def from_dict(cls, data: Mapping[str, Any]) -> LearningEpisode:
73
+ """Restore configuration with ``None`` placeholders for dataset objects."""
74
+ try:
75
+ names = data["evaluation_names"]
76
+ evaluations = {name: None for name in names}
77
+ return cls(
78
+ name=data["name"],
79
+ train=None,
80
+ evaluations=evaluations,
81
+ metadata=data.get("metadata", {}),
82
+ episode_id=data.get("episode_id"),
83
+ )
84
+ except (KeyError, TypeError) as error:
85
+ raise ValidationError("malformed serialized LearningEpisode") from error
86
+
87
+ def to_json(
88
+ self,
89
+ destination: str | PathLike[str] | None = None,
90
+ *,
91
+ indent: int | None = 2,
92
+ ) -> str:
93
+ return write_json(self.to_dict(), destination, indent=indent)
94
+
95
+ @classmethod
96
+ def from_json(cls, source: str | PathLike[str]) -> LearningEpisode:
97
+ return cls.from_dict(read_json(source))
98
+
99
+ def __repr__(self) -> str:
100
+ return (
101
+ f"LearningEpisode(name={self.name!r}, train={type(self.train).__name__}, "
102
+ f"evaluations={list(self.evaluations)!r}, episode_id={self.episode_id!r})"
103
+ )
104
+
105
+
106
+ def _episode_identity_key(episode: LearningEpisode) -> EpisodeIdentityKey:
107
+ """Return the conservative, dataset-independent identity used by collections."""
108
+ if episode.episode_id is not None:
109
+ return ("episode_id", episode.episode_id)
110
+ return ("configuration", json_dumps(episode.to_dict(), indent=None))
adaptcompile/errors.py ADDED
@@ -0,0 +1,13 @@
1
+ """Package-specific exceptions."""
2
+
3
+
4
+ class AdaptCompileError(Exception):
5
+ """Base exception for errors raised by adaptcompile."""
6
+
7
+
8
+ class ValidationError(AdaptCompileError, ValueError):
9
+ """Raised when an adaptcompile value is malformed or inconsistent."""
10
+
11
+
12
+ class SerializationError(AdaptCompileError, TypeError):
13
+ """Raised when a record cannot be represented safely as JSON."""
adaptcompile/family.py ADDED
@@ -0,0 +1,103 @@
1
+ """Conceptual adaptation program families."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass, field
9
+ from os import PathLike
10
+ from typing import Any
11
+
12
+ from ._validation import nonempty_name
13
+ from .errors import ValidationError
14
+ from .serialization import (
15
+ immutable_json_mapping,
16
+ read_json,
17
+ to_json_safe,
18
+ write_json,
19
+ )
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ProgramFamily:
24
+ """A conceptual adaptation choice, before model-specific realization.
25
+
26
+ Structural equality includes every field. ``fingerprint`` identifies only the
27
+ conceptual semantics expressed by ``method`` and ``parameters``.
28
+ """
29
+
30
+ name: str
31
+ method: str
32
+ parameters: Mapping[str, Any]
33
+ metadata: Mapping[str, Any] = field(default_factory=dict)
34
+
35
+ def __post_init__(self) -> None:
36
+ object.__setattr__(self, "name", nonempty_name(self.name, field="name"))
37
+ object.__setattr__(self, "method", nonempty_name(self.method, field="method"))
38
+ object.__setattr__(
39
+ self,
40
+ "parameters",
41
+ immutable_json_mapping(self.parameters, location="parameters"),
42
+ )
43
+ object.__setattr__(
44
+ self,
45
+ "metadata",
46
+ immutable_json_mapping(self.metadata, location="metadata"),
47
+ )
48
+
49
+ @property
50
+ def fingerprint(self) -> str:
51
+ """Stable SHA-256 identity derived from method and parameters only."""
52
+ conceptual = {
53
+ "method": self.method,
54
+ "parameters": to_json_safe(self.parameters, location="parameters"),
55
+ }
56
+ canonical = json.dumps(
57
+ conceptual,
58
+ sort_keys=True,
59
+ separators=(",", ":"),
60
+ allow_nan=False,
61
+ )
62
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
63
+
64
+ def to_dict(self) -> dict[str, Any]:
65
+ return {
66
+ "name": self.name,
67
+ "method": self.method,
68
+ "parameters": to_json_safe(self.parameters, location="parameters"),
69
+ "metadata": to_json_safe(self.metadata, location="metadata"),
70
+ }
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: Mapping[str, Any]) -> ProgramFamily:
74
+ try:
75
+ return cls(
76
+ name=data["name"],
77
+ method=data["method"],
78
+ parameters=data["parameters"],
79
+ metadata=data.get("metadata", {}),
80
+ )
81
+ except (KeyError, TypeError) as error:
82
+ raise ValidationError("malformed serialized ProgramFamily") from error
83
+
84
+ def to_json(
85
+ self,
86
+ destination: str | PathLike[str] | None = None,
87
+ *,
88
+ indent: int | None = 2,
89
+ ) -> str:
90
+ return write_json(self.to_dict(), destination, indent=indent)
91
+
92
+ @classmethod
93
+ def from_json(cls, source: str | PathLike[str]) -> ProgramFamily:
94
+ return cls.from_dict(read_json(source))
95
+
96
+ def __hash__(self) -> int:
97
+ canonical = json.dumps(
98
+ to_json_safe(self.to_dict()),
99
+ sort_keys=True,
100
+ separators=(",", ":"),
101
+ allow_nan=False,
102
+ )
103
+ return hash(canonical)
@@ -0,0 +1,111 @@
1
+ """Multidimensional behavioral geometry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from os import PathLike
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ from ._validation import score_mapping
10
+ from .errors import ValidationError
11
+ from .serialization import read_json, write_json
12
+
13
+ if TYPE_CHECKING:
14
+ from pandas import DataFrame
15
+
16
+
17
+ class AdaptationGeometry(Mapping[str, float]):
18
+ """An immutable, arbitrary-dimensional mapping of behavioral measurements."""
19
+
20
+ __slots__ = ("_metrics",)
21
+
22
+ def __init__(self, metrics: Mapping[str, float]) -> None:
23
+ normalized = score_mapping(metrics, field="metrics")
24
+ if not normalized:
25
+ raise ValidationError("geometry must contain at least one metric")
26
+ self._metrics = normalized
27
+
28
+ @property
29
+ def metrics(self) -> Mapping[str, float]:
30
+ """A read-only metric mapping."""
31
+ return self._metrics
32
+
33
+ @property
34
+ def metric_names(self) -> tuple[str, ...]:
35
+ return tuple(self._metrics)
36
+
37
+ def __getitem__(self, name: str) -> float:
38
+ try:
39
+ return self._metrics[name]
40
+ except KeyError as error:
41
+ raise KeyError(f"unknown geometry metric {name!r}") from error
42
+
43
+ def __iter__(self) -> Iterator[str]:
44
+ return iter(self._metrics)
45
+
46
+ def __len__(self) -> int:
47
+ return len(self._metrics)
48
+
49
+ def __repr__(self) -> str:
50
+ return f"AdaptationGeometry({dict(self._metrics)!r})"
51
+
52
+ def __eq__(self, other: object) -> bool:
53
+ if not isinstance(other, AdaptationGeometry):
54
+ return NotImplemented
55
+ return dict(self._metrics) == dict(other._metrics)
56
+
57
+ def __hash__(self) -> int:
58
+ return hash(frozenset(self._metrics.items()))
59
+
60
+ def to_dict(self) -> dict[str, float]:
61
+ return dict(self._metrics)
62
+
63
+ @classmethod
64
+ def from_dict(cls, data: Mapping[str, Any]) -> AdaptationGeometry:
65
+ return cls(data)
66
+
67
+ def to_json(
68
+ self,
69
+ destination: str | PathLike[str] | None = None,
70
+ *,
71
+ indent: int | None = 2,
72
+ ) -> str:
73
+ return write_json(self.to_dict(), destination, indent=indent)
74
+
75
+ @classmethod
76
+ def from_json(cls, source: str | PathLike[str]) -> AdaptationGeometry:
77
+ return cls.from_dict(read_json(source))
78
+
79
+ def compare(self, other: AdaptationGeometry) -> AdaptationGeometry:
80
+ """Return ``self - other`` after requiring identical metric dimensions."""
81
+ self._require_comparable(other)
82
+ return AdaptationGeometry(
83
+ {name: value - other[name] for name, value in self._metrics.items()}
84
+ )
85
+
86
+ def delta(self, baseline: AdaptationGeometry) -> AdaptationGeometry:
87
+ """Alias for :meth:`compare`, naming *baseline* explicitly."""
88
+ return self.compare(baseline)
89
+
90
+ def to_dataframe(self) -> DataFrame:
91
+ """Return a one-row pandas DataFrame (requires the ``dataframe`` extra)."""
92
+ try:
93
+ from pandas import DataFrame
94
+ except ImportError as error: # pragma: no cover - depends on environment
95
+ raise ImportError(
96
+ "to_dataframe() requires pandas; install adaptcompile[dataframe]"
97
+ ) from error
98
+ return cast("DataFrame", DataFrame([self.to_dict()]))
99
+
100
+ def _require_comparable(self, other: AdaptationGeometry) -> None:
101
+ if not isinstance(other, AdaptationGeometry):
102
+ raise TypeError("other must be an AdaptationGeometry")
103
+ own = set(self._metrics)
104
+ theirs = set(other._metrics)
105
+ if own != theirs:
106
+ missing = sorted(own - theirs)
107
+ extra = sorted(theirs - own)
108
+ raise ValidationError(
109
+ "geometry metrics do not match "
110
+ f"(missing from other={missing}, extra in other={extra})"
111
+ )
adaptcompile/model.py ADDED
@@ -0,0 +1,93 @@
1
+ """Declarative model and base-state context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from os import PathLike
8
+ from typing import Any
9
+
10
+ from ._validation import nonempty_name
11
+ from .errors import ValidationError
12
+ from .serialization import immutable_json_mapping, read_json, to_json_safe, write_json
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ModelContext:
17
+ """A declarative description of the pre-adaptation model/base state.
18
+
19
+ This record identifies a model state; it does not contain, load, or inspect
20
+ model weights and has no dependency on a model framework.
21
+ """
22
+
23
+ model_id: str
24
+ revision: str | None = None
25
+ base_state_id: str | None = None
26
+ metadata: Mapping[str, Any] = field(default_factory=dict)
27
+
28
+ def __post_init__(self) -> None:
29
+ object.__setattr__(
30
+ self,
31
+ "model_id",
32
+ nonempty_name(self.model_id, field="model_id"),
33
+ )
34
+ if self.revision is not None:
35
+ object.__setattr__(
36
+ self,
37
+ "revision",
38
+ nonempty_name(self.revision, field="revision"),
39
+ )
40
+ if self.base_state_id is not None:
41
+ object.__setattr__(
42
+ self,
43
+ "base_state_id",
44
+ nonempty_name(self.base_state_id, field="base_state_id"),
45
+ )
46
+ object.__setattr__(
47
+ self,
48
+ "metadata",
49
+ immutable_json_mapping(self.metadata, location="metadata"),
50
+ )
51
+
52
+ @property
53
+ def identity_key(self) -> tuple[str, str | None, str | None]:
54
+ """Identity used to hold model/base state fixed within a study."""
55
+ return (self.model_id, self.revision, self.base_state_id)
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ return {
59
+ "model_id": self.model_id,
60
+ "revision": self.revision,
61
+ "base_state_id": self.base_state_id,
62
+ "metadata": to_json_safe(self.metadata, location="metadata"),
63
+ }
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: Mapping[str, Any]) -> ModelContext:
67
+ try:
68
+ return cls(
69
+ model_id=data["model_id"],
70
+ revision=data.get("revision"),
71
+ base_state_id=data.get("base_state_id"),
72
+ metadata=data.get("metadata", {}),
73
+ )
74
+ except (KeyError, TypeError) as error:
75
+ raise ValidationError("malformed serialized ModelContext") from error
76
+
77
+ def to_json(
78
+ self,
79
+ destination: str | PathLike[str] | None = None,
80
+ *,
81
+ indent: int | None = 2,
82
+ ) -> str:
83
+ return write_json(self.to_dict(), destination, indent=indent)
84
+
85
+ @classmethod
86
+ def from_json(cls, source: str | PathLike[str]) -> ModelContext:
87
+ return cls.from_dict(read_json(source))
88
+
89
+ def __repr__(self) -> str:
90
+ return (
91
+ f"ModelContext(model_id={self.model_id!r}, revision={self.revision!r}, "
92
+ f"base_state_id={self.base_state_id!r})"
93
+ )