Spaces:
Runtime error
Runtime error
| """ | |
| analysis.trajectory_extractor | |
| ============================= | |
| Convert raw WorldSmithAI simulation history into structured trajectory data. | |
| This module is intentionally placed above the simulation engine. It consumes | |
| world history, metric logs, and event logs without mutating runtime agents, | |
| resources, behaviors, policies, schedulers, or worlds. | |
| The extractor is designed to be tolerant of several history formats because the | |
| core engine may evolve independently. It accepts either: | |
| - a world-like object with ``history`` | |
| - a raw sequence of snapshot mappings | |
| - a single snapshot mapping | |
| The output is a ``TrajectoryData`` dataclass consumed by later analysis modules. | |
| Example | |
| ------- | |
| extractor = TrajectoryExtractor() | |
| trajectory = extractor.extract(world) | |
| print(trajectory.step_count) | |
| print(trajectory.agent_type_counts) | |
| print(trajectory.population_timeseries) | |
| Architecture | |
| ------------ | |
| world.history | |
| ↓ | |
| TrajectoryExtractor.extract(...) | |
| ↓ | |
| TrajectoryData | |
| ↓ | |
| simulation_analyzer / report_generator / scenario comparator | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import logging | |
| import math | |
| from collections import defaultdict | |
| from collections.abc import Mapping, Sequence | |
| from dataclasses import dataclass, field | |
| from typing import Any, Protocol | |
| logger = logging.getLogger(__name__) | |
| class WorldHistoryProtocol(Protocol): | |
| """Structural protocol for world-like objects with history.""" | |
| history: Any | |
| step_count: int | |
| class TrajectoryExtractionConfig: | |
| """Configuration for trajectory extraction. | |
| Attributes: | |
| count_alive_only: | |
| If true, population counts exclude agents with ``alive=False``. | |
| include_snapshot_events: | |
| If true, event-like records inside each snapshot are added to the | |
| normalized event log. | |
| include_snapshot_metrics: | |
| If true, metric-like records inside each snapshot are added to the | |
| normalized metric history. | |
| preserve_unknown_snapshot_fields: | |
| If true, selected unrecognized top-level snapshot keys are preserved | |
| in metadata for debugging. | |
| max_preserved_unknown_values: | |
| Maximum number of unknown snapshot values preserved in metadata. | |
| """ | |
| count_alive_only: bool = True | |
| include_snapshot_events: bool = True | |
| include_snapshot_metrics: bool = True | |
| preserve_unknown_snapshot_fields: bool = False | |
| max_preserved_unknown_values: int = 20 | |
| class TrajectoryData: | |
| """Structured simulation trajectory data. | |
| Attributes: | |
| population_timeseries: | |
| Mapping from step to counts by agent type. | |
| resource_timeseries: | |
| Mapping from step to total resource amount by resource type. | |
| event_log: | |
| Normalized event records. Each record should include a ``step`` key | |
| when the step is known. | |
| metric_history: | |
| Mapping from metric name to a list of per-step metric records. | |
| agent_type_counts: | |
| Final observed agent counts by type. | |
| step_count: | |
| Number of simulated steps represented by the trajectory. This is | |
| the highest observed step plus one when possible. | |
| first_step: | |
| First observed step, or ``None`` when there is no history. | |
| last_step: | |
| Last observed step, or ``None`` when there is no history. | |
| snapshot_count: | |
| Number of snapshots consumed. | |
| metadata: | |
| Additional extraction diagnostics. | |
| """ | |
| population_timeseries: Mapping[int, Mapping[str, int]] = field(default_factory=dict) | |
| resource_timeseries: Mapping[int, Mapping[str, float]] = field(default_factory=dict) | |
| event_log: tuple[Mapping[str, Any], ...] = field(default_factory=tuple) | |
| metric_history: Mapping[str, tuple[Mapping[str, Any], ...]] = field(default_factory=dict) | |
| agent_type_counts: Mapping[str, int] = field(default_factory=dict) | |
| step_count: int = 0 | |
| first_step: int | None = None | |
| last_step: int | None = None | |
| snapshot_count: int = 0 | |
| metadata: Mapping[str, Any] = field(default_factory=dict) | |
| def is_empty(self) -> bool: | |
| """Return whether this trajectory contains no snapshots.""" | |
| return self.snapshot_count == 0 | |
| def final_population(self) -> Mapping[str, int]: | |
| """Return the final population count by agent type.""" | |
| if self.last_step is None: | |
| return {} | |
| return dict(self.population_timeseries.get(self.last_step, {})) | |
| def final_resources(self) -> Mapping[str, float]: | |
| """Return the final resource totals by resource type.""" | |
| if self.last_step is None: | |
| return {} | |
| return dict(self.resource_timeseries.get(self.last_step, {})) | |
| def population_delta(self) -> dict[str, int]: | |
| """Return final-minus-initial population delta by agent type.""" | |
| if self.first_step is None or self.last_step is None: | |
| return {} | |
| initial = self.population_timeseries.get(self.first_step, {}) | |
| final = self.population_timeseries.get(self.last_step, {}) | |
| keys = set(initial.keys()) | set(final.keys()) | |
| return { | |
| key: int(final.get(key, 0)) - int(initial.get(key, 0)) | |
| for key in sorted(keys) | |
| } | |
| def resource_delta(self) -> dict[str, float]: | |
| """Return final-minus-initial resource amount delta by resource type.""" | |
| if self.first_step is None or self.last_step is None: | |
| return {} | |
| initial = self.resource_timeseries.get(self.first_step, {}) | |
| final = self.resource_timeseries.get(self.last_step, {}) | |
| keys = set(initial.keys()) | set(final.keys()) | |
| return { | |
| key: float(final.get(key, 0.0)) - float(initial.get(key, 0.0)) | |
| for key in sorted(keys) | |
| } | |
| def to_dict(self) -> dict[str, Any]: | |
| """Return a JSON-friendly representation of the trajectory.""" | |
| return { | |
| "population_timeseries": { | |
| str(step): dict(counts) | |
| for step, counts in sorted(self.population_timeseries.items()) | |
| }, | |
| "resource_timeseries": { | |
| str(step): dict(amounts) | |
| for step, amounts in sorted(self.resource_timeseries.items()) | |
| }, | |
| "event_log": [dict(event) for event in self.event_log], | |
| "metric_history": { | |
| metric_name: [dict(record) for record in records] | |
| for metric_name, records in self.metric_history.items() | |
| }, | |
| "agent_type_counts": dict(self.agent_type_counts), | |
| "step_count": self.step_count, | |
| "first_step": self.first_step, | |
| "last_step": self.last_step, | |
| "snapshot_count": self.snapshot_count, | |
| "population_delta": self.population_delta(), | |
| "resource_delta": self.resource_delta(), | |
| "metadata": copy.deepcopy(dict(self.metadata)), | |
| } | |
| class TrajectoryExtractor: | |
| """Extract structured trajectory data from completed simulations.""" | |
| config: TrajectoryExtractionConfig = field(default_factory=TrajectoryExtractionConfig) | |
| def extract( | |
| self, | |
| world_or_history: Any, | |
| *, | |
| metric_history: Mapping[str, Any] | Sequence[Any] | None = None, | |
| event_log: Sequence[Any] | Mapping[str, Any] | None = None, | |
| ) -> TrajectoryData: | |
| """Extract trajectory data from a world-like object or raw history. | |
| Args: | |
| world_or_history: | |
| Either a world-like object with ``history`` or a raw history | |
| object. | |
| metric_history: | |
| Optional external metric history to merge with snapshot metrics. | |
| event_log: | |
| Optional external event log to merge with snapshot events. | |
| Returns: | |
| Normalized ``TrajectoryData``. | |
| """ | |
| world = self._as_world_like(world_or_history) | |
| raw_history = self._history_from_input(world_or_history) | |
| snapshots = self._normalize_history(raw_history) | |
| if not snapshots and world is not None: | |
| current_snapshot = self._snapshot_from_world(world) | |
| if current_snapshot: | |
| snapshots = (current_snapshot,) | |
| population_timeseries: dict[int, dict[str, int]] = {} | |
| resource_timeseries: dict[int, dict[str, float]] = {} | |
| normalized_event_log: list[dict[str, Any]] = [] | |
| normalized_metric_history: dict[str, list[dict[str, Any]]] = defaultdict(list) | |
| unknown_snapshot_metadata: list[dict[str, Any]] = [] | |
| for index, raw_snapshot in enumerate(snapshots): | |
| snapshot = self._mapping_from_any(raw_snapshot) | |
| step = self._step_from_snapshot(snapshot, fallback=index) | |
| population_counts = self._population_counts_from_snapshot(snapshot) | |
| resource_amounts = self._resource_amounts_from_snapshot(snapshot) | |
| population_timeseries[step] = population_counts | |
| resource_timeseries[step] = resource_amounts | |
| if self.config.include_snapshot_events: | |
| self._append_snapshot_events( | |
| snapshot=snapshot, | |
| step=step, | |
| output=normalized_event_log, | |
| ) | |
| if self.config.include_snapshot_metrics: | |
| self._append_snapshot_metrics( | |
| snapshot=snapshot, | |
| step=step, | |
| output=normalized_metric_history, | |
| ) | |
| if self.config.preserve_unknown_snapshot_fields: | |
| preserved = self._preserve_unknown_snapshot_fields(snapshot, step=step) | |
| if preserved: | |
| unknown_snapshot_metadata.append(preserved) | |
| self._append_external_events( | |
| event_log=event_log, | |
| world=world, | |
| output=normalized_event_log, | |
| ) | |
| self._append_external_metrics( | |
| metric_history=metric_history, | |
| world=world, | |
| output=normalized_metric_history, | |
| ) | |
| observed_steps = sorted(population_timeseries.keys() | resource_timeseries.keys()) | |
| first_step = observed_steps[0] if observed_steps else None | |
| last_step = observed_steps[-1] if observed_steps else None | |
| if last_step is None: | |
| step_count = int(getattr(world, "step_count", 0)) if world is not None else 0 | |
| else: | |
| step_count = max(last_step + 1, int(getattr(world, "step_count", 0)) if world is not None else 0) | |
| final_counts = ( | |
| population_timeseries.get(last_step, {}) | |
| if last_step is not None | |
| else {} | |
| ) | |
| metadata: dict[str, Any] = { | |
| "source": "world" if world is not None else "history", | |
| "history_length": len(snapshots), | |
| "count_alive_only": self.config.count_alive_only, | |
| } | |
| if unknown_snapshot_metadata: | |
| metadata["unknown_snapshot_fields"] = unknown_snapshot_metadata[ | |
| : self.config.max_preserved_unknown_values | |
| ] | |
| return TrajectoryData( | |
| population_timeseries={ | |
| step: dict(counts) | |
| for step, counts in sorted(population_timeseries.items()) | |
| }, | |
| resource_timeseries={ | |
| step: dict(amounts) | |
| for step, amounts in sorted(resource_timeseries.items()) | |
| }, | |
| event_log=tuple(normalized_event_log), | |
| metric_history={ | |
| metric_name: tuple(records) | |
| for metric_name, records in sorted(normalized_metric_history.items()) | |
| }, | |
| agent_type_counts=dict(final_counts), | |
| step_count=step_count, | |
| first_step=first_step, | |
| last_step=last_step, | |
| snapshot_count=len(snapshots), | |
| metadata=metadata, | |
| ) | |
| def extract_from_world(self, world: WorldHistoryProtocol) -> TrajectoryData: | |
| """Extract trajectory data directly from a world-like object.""" | |
| return self.extract(world) | |
| def extract_from_history(self, history: Sequence[Any] | Mapping[str, Any]) -> TrajectoryData: | |
| """Extract trajectory data directly from a raw history object.""" | |
| return self.extract(history) | |
| def _as_world_like(value: Any) -> Any | None: | |
| """Return value when it appears to be a world-like object.""" | |
| if hasattr(value, "history"): | |
| return value | |
| return None | |
| def _history_from_input(value: Any) -> Any: | |
| """Return raw history from a world-like object or raw history input.""" | |
| if hasattr(value, "history"): | |
| return getattr(value, "history") | |
| return value | |
| def _normalize_history(self, raw_history: Any) -> tuple[Any, ...]: | |
| """Normalize raw history into a tuple of snapshots.""" | |
| if raw_history is None: | |
| return () | |
| if isinstance(raw_history, Mapping): | |
| if "history" in raw_history: | |
| return self._normalize_history(raw_history["history"]) | |
| if "snapshots" in raw_history: | |
| return self._normalize_history(raw_history["snapshots"]) | |
| return (raw_history,) | |
| if isinstance(raw_history, Sequence) and not isinstance(raw_history, (str, bytes)): | |
| return tuple(raw_history) | |
| logger.warning( | |
| "TrajectoryExtractor received unsupported history type: %s", | |
| raw_history.__class__.__name__, | |
| ) | |
| return () | |
| def _snapshot_from_world(self, world: Any) -> dict[str, Any]: | |
| """Build a single best-effort snapshot from a world-like object.""" | |
| snapshot_method = getattr(world, "snapshot", None) | |
| if callable(snapshot_method): | |
| try: | |
| snapshot = snapshot_method() | |
| if isinstance(snapshot, Mapping): | |
| return dict(snapshot) | |
| except Exception: | |
| logger.debug("world.snapshot() failed during trajectory extraction", exc_info=True) | |
| to_dict = getattr(world, "to_dict", None) | |
| if callable(to_dict): | |
| try: | |
| snapshot = to_dict() | |
| if isinstance(snapshot, Mapping): | |
| return dict(snapshot) | |
| except Exception: | |
| logger.debug("world.to_dict() failed during trajectory extraction", exc_info=True) | |
| snapshot: dict[str, Any] = { | |
| "step": getattr(world, "step_count", 0), | |
| "agents": getattr(world, "agents", {}), | |
| "resources": getattr(world, "resources", {}), | |
| } | |
| metrics = getattr(world, "metrics", None) | |
| if metrics is not None: | |
| snapshot["metrics"] = metrics | |
| return snapshot | |
| def _population_counts_from_snapshot(self, snapshot: Mapping[str, Any]) -> dict[str, int]: | |
| """Extract population counts by agent type from a snapshot.""" | |
| if "agent_type_counts" in snapshot and isinstance(snapshot["agent_type_counts"], Mapping): | |
| return { | |
| str(agent_type): int(count) | |
| for agent_type, count in snapshot["agent_type_counts"].items() | |
| if self._is_numeric(count) | |
| } | |
| if "population" in snapshot and isinstance(snapshot["population"], Mapping): | |
| return { | |
| str(agent_type): int(count) | |
| for agent_type, count in snapshot["population"].items() | |
| if self._is_numeric(count) | |
| } | |
| agents = self._collection_values( | |
| self._first_present( | |
| snapshot, | |
| ("agents", "agent_states", "agent_snapshots"), | |
| default=(), | |
| ) | |
| ) | |
| counts: dict[str, int] = defaultdict(int) | |
| for agent in agents: | |
| agent_type = self._field(agent, "type", "agent_type", "kind", default="unknown") | |
| alive = self._field(agent, "alive", default=True) | |
| if self.config.count_alive_only and alive is False: | |
| continue | |
| counts[str(agent_type)] += 1 | |
| return dict(sorted(counts.items())) | |
| def _resource_amounts_from_snapshot(self, snapshot: Mapping[str, Any]) -> dict[str, float]: | |
| """Extract resource total amounts by resource type from a snapshot.""" | |
| for key in ("resource_timeseries", "resource_type_amounts", "resource_amounts"): | |
| value = snapshot.get(key) | |
| if isinstance(value, Mapping): | |
| return { | |
| str(resource_type): float(amount) | |
| for resource_type, amount in value.items() | |
| if self._is_numeric(amount) | |
| } | |
| resources = self._collection_values( | |
| self._first_present( | |
| snapshot, | |
| ("resources", "resource_states", "resource_snapshots"), | |
| default=(), | |
| ) | |
| ) | |
| amounts: dict[str, float] = defaultdict(float) | |
| for resource in resources: | |
| resource_type = self._field(resource, "type", "resource_type", "kind", default="unknown") | |
| amount = self._field(resource, "amount", "quantity", "value", default=0.0) | |
| if self._is_numeric(amount): | |
| amounts[str(resource_type)] += float(amount) | |
| return dict(sorted(amounts.items())) | |
| def _append_snapshot_events( | |
| self, | |
| *, | |
| snapshot: Mapping[str, Any], | |
| step: int, | |
| output: list[dict[str, Any]], | |
| ) -> None: | |
| """Append normalized event records from a snapshot.""" | |
| raw_events = self._first_present( | |
| snapshot, | |
| ("event_log", "events_processed", "events_fired", "events"), | |
| default=(), | |
| ) | |
| for record in self._collection_values(raw_events): | |
| normalized = self._normalize_event_record(record, default_step=step) | |
| if normalized: | |
| output.append(normalized) | |
| def _append_snapshot_metrics( | |
| self, | |
| *, | |
| snapshot: Mapping[str, Any], | |
| step: int, | |
| output: dict[str, list[dict[str, Any]]], | |
| ) -> None: | |
| """Append metric records from a snapshot.""" | |
| raw_metrics = self._first_present( | |
| snapshot, | |
| ("metrics", "metric_values", "metric_history"), | |
| default=None, | |
| ) | |
| if raw_metrics is None: | |
| return | |
| self._append_metric_records(raw_metrics, default_step=step, output=output) | |
| def _append_external_events( | |
| self, | |
| *, | |
| event_log: Sequence[Any] | Mapping[str, Any] | None, | |
| world: Any | None, | |
| output: list[dict[str, Any]], | |
| ) -> None: | |
| """Append externally supplied or world-level event log records.""" | |
| raw_event_log = event_log | |
| if raw_event_log is None and world is not None: | |
| raw_event_log = self._first_present_from_object( | |
| world, | |
| ("event_log", "events_processed", "events_fired"), | |
| default=None, | |
| ) | |
| if raw_event_log is None: | |
| return | |
| for record in self._collection_values(raw_event_log): | |
| normalized = self._normalize_event_record(record, default_step=None) | |
| if normalized: | |
| output.append(normalized) | |
| def _append_external_metrics( | |
| self, | |
| *, | |
| metric_history: Mapping[str, Any] | Sequence[Any] | None, | |
| world: Any | None, | |
| output: dict[str, list[dict[str, Any]]], | |
| ) -> None: | |
| """Append externally supplied or world-level metric history.""" | |
| raw_metric_history = metric_history | |
| if raw_metric_history is None and world is not None: | |
| raw_metric_history = self._first_present_from_object( | |
| world, | |
| ("metric_history", "metrics_history", "metrics"), | |
| default=None, | |
| ) | |
| if raw_metric_history is None: | |
| return | |
| self._append_metric_records(raw_metric_history, default_step=None, output=output) | |
| def _append_metric_records( | |
| self, | |
| raw_metrics: Any, | |
| *, | |
| default_step: int | None, | |
| output: dict[str, list[dict[str, Any]]], | |
| ) -> None: | |
| """Normalize and append metric records.""" | |
| if isinstance(raw_metrics, Mapping): | |
| for metric_name, metric_value in raw_metrics.items(): | |
| if isinstance(metric_value, Sequence) and not isinstance(metric_value, (str, bytes, Mapping)): | |
| for item in metric_value: | |
| output[str(metric_name)].append( | |
| self._normalize_metric_record( | |
| item, | |
| metric_name=str(metric_name), | |
| default_step=default_step, | |
| ) | |
| ) | |
| else: | |
| output[str(metric_name)].append( | |
| self._normalize_metric_record( | |
| metric_value, | |
| metric_name=str(metric_name), | |
| default_step=default_step, | |
| ) | |
| ) | |
| return | |
| if isinstance(raw_metrics, Sequence) and not isinstance(raw_metrics, (str, bytes)): | |
| for item in raw_metrics: | |
| item_mapping = self._mapping_from_any(item) | |
| metric_name = str( | |
| self._first_present( | |
| item_mapping, | |
| ("name", "metric", "metric_name"), | |
| default="unknown_metric", | |
| ) | |
| ) | |
| output[metric_name].append( | |
| self._normalize_metric_record( | |
| item_mapping, | |
| metric_name=metric_name, | |
| default_step=default_step, | |
| ) | |
| ) | |
| def _normalize_event_record( | |
| self, | |
| record: Any, | |
| *, | |
| default_step: int | None, | |
| ) -> dict[str, Any]: | |
| """Normalize one event record.""" | |
| mapping = self._mapping_from_any(record) | |
| if not mapping: | |
| return {} | |
| if default_step is not None: | |
| mapping.setdefault("step", default_step) | |
| event_name = self._first_present( | |
| mapping, | |
| ("name", "event", "event_name", "type"), | |
| default=None, | |
| ) | |
| if event_name is not None: | |
| mapping.setdefault("name", str(event_name)) | |
| return self._json_safe_mapping(mapping) | |
| def _normalize_metric_record( | |
| self, | |
| record: Any, | |
| *, | |
| metric_name: str, | |
| default_step: int | None, | |
| ) -> dict[str, Any]: | |
| """Normalize one metric record.""" | |
| if isinstance(record, Mapping): | |
| output = self._json_safe_mapping(record) | |
| else: | |
| output = {"value": self._json_safe(record)} | |
| output.setdefault("name", metric_name) | |
| if default_step is not None: | |
| output.setdefault("step", default_step) | |
| return output | |
| def _preserve_unknown_snapshot_fields( | |
| self, | |
| snapshot: Mapping[str, Any], | |
| *, | |
| step: int, | |
| ) -> dict[str, Any]: | |
| """Preserve selected unknown fields from a snapshot for diagnostics.""" | |
| known_keys = { | |
| "step", | |
| "step_count", | |
| "time", | |
| "tick", | |
| "agents", | |
| "agent_states", | |
| "agent_snapshots", | |
| "agent_type_counts", | |
| "population", | |
| "resources", | |
| "resource_states", | |
| "resource_snapshots", | |
| "resource_timeseries", | |
| "resource_type_amounts", | |
| "resource_amounts", | |
| "events", | |
| "event_log", | |
| "events_processed", | |
| "events_fired", | |
| "metrics", | |
| "metric_values", | |
| "metric_history", | |
| } | |
| unknown: dict[str, Any] = {} | |
| for key, value in snapshot.items(): | |
| if key in known_keys: | |
| continue | |
| unknown[str(key)] = self._json_safe(value) | |
| if not unknown: | |
| return {} | |
| return { | |
| "step": step, | |
| "fields": unknown, | |
| } | |
| def _step_from_snapshot(snapshot: Mapping[str, Any], *, fallback: int) -> int: | |
| """Return step value from snapshot with a fallback index.""" | |
| for key in ("step", "step_count", "time", "tick"): | |
| if key in snapshot and TrajectoryExtractor._is_numeric(snapshot[key]): | |
| return int(snapshot[key]) | |
| return int(fallback) | |
| def _collection_values(value: Any) -> tuple[Any, ...]: | |
| """Return collection values from mapping or sequence.""" | |
| if value is None: | |
| return () | |
| if isinstance(value, Mapping): | |
| return tuple(value.values()) | |
| if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): | |
| return tuple(value) | |
| return (value,) | |
| def _mapping_from_any(value: Any) -> dict[str, Any]: | |
| """Return a mapping representation of an arbitrary value.""" | |
| if value is None: | |
| return {} | |
| if isinstance(value, Mapping): | |
| return dict(value) | |
| for method_name in ("to_dict", "snapshot", "model_dump"): | |
| method = getattr(value, method_name, None) | |
| if callable(method): | |
| try: | |
| result = method() | |
| if isinstance(result, Mapping): | |
| return dict(result) | |
| except Exception: | |
| logger.debug( | |
| "Could not convert %s using %s()", | |
| value.__class__.__name__, | |
| method_name, | |
| exc_info=True, | |
| ) | |
| output: dict[str, Any] = {} | |
| for attribute in ("id", "name", "type", "amount", "position", "alive", "state", "metadata"): | |
| if hasattr(value, attribute): | |
| output[attribute] = getattr(value, attribute) | |
| return output | |
| def _field(value: Any, *names: str, default: Any = None) -> Any: | |
| """Read the first present field from mapping or object.""" | |
| if isinstance(value, Mapping): | |
| for name in names: | |
| if name in value: | |
| return value[name] | |
| return default | |
| for name in names: | |
| if hasattr(value, name): | |
| return getattr(value, name) | |
| return default | |
| def _first_present(mapping: Mapping[str, Any], keys: Sequence[str], *, default: Any) -> Any: | |
| """Return first present mapping value.""" | |
| for key in keys: | |
| if key in mapping and mapping[key] is not None: | |
| return mapping[key] | |
| return default | |
| def _first_present_from_object(value: Any, keys: Sequence[str], *, default: Any) -> Any: | |
| """Return first present object attribute value.""" | |
| for key in keys: | |
| if hasattr(value, key): | |
| candidate = getattr(value, key) | |
| if candidate is not None: | |
| return candidate | |
| return default | |
| def _is_numeric(value: Any) -> bool: | |
| """Return whether value is a finite int or float but not bool.""" | |
| if isinstance(value, bool): | |
| return False | |
| if not isinstance(value, (int, float)): | |
| return False | |
| return math.isfinite(float(value)) | |
| def _json_safe_mapping(cls, mapping: Mapping[str, Any]) -> dict[str, Any]: | |
| """Return JSON-safe copy of a mapping.""" | |
| return { | |
| str(key): cls._json_safe(value) | |
| for key, value in mapping.items() | |
| } | |
| def _json_safe(cls, value: Any) -> Any: | |
| """Return a JSON-friendly representation.""" | |
| if value is None or isinstance(value, (str, bool)): | |
| return value | |
| if isinstance(value, int) and not isinstance(value, bool): | |
| return value | |
| if isinstance(value, float): | |
| return value if math.isfinite(value) else None | |
| if isinstance(value, Mapping): | |
| return { | |
| str(key): cls._json_safe(item) | |
| for key, item in value.items() | |
| } | |
| if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): | |
| return [cls._json_safe(item) for item in value] | |
| for method_name in ("to_dict", "snapshot", "model_dump"): | |
| method = getattr(value, method_name, None) | |
| if callable(method): | |
| try: | |
| return cls._json_safe(method()) | |
| except Exception: | |
| logger.debug( | |
| "Could not JSON-normalize %s using %s()", | |
| value.__class__.__name__, | |
| method_name, | |
| exc_info=True, | |
| ) | |
| if hasattr(value, "tolist") and callable(value.tolist): | |
| try: | |
| return cls._json_safe(value.tolist()) | |
| except Exception: | |
| logger.debug("Could not JSON-normalize tolist() value", exc_info=True) | |
| return repr(value) | |
| def extract_trajectory( | |
| world_or_history: Any, | |
| *, | |
| metric_history: Mapping[str, Any] | Sequence[Any] | None = None, | |
| event_log: Sequence[Any] | Mapping[str, Any] | None = None, | |
| config: TrajectoryExtractionConfig | None = None, | |
| ) -> TrajectoryData: | |
| """Convenience function for extracting trajectory data.""" | |
| extractor = TrajectoryExtractor(config=config or TrajectoryExtractionConfig()) | |
| return extractor.extract( | |
| world_or_history, | |
| metric_history=metric_history, | |
| event_log=event_log, | |
| ) | |
| __all__ = [ | |
| "TrajectoryData", | |
| "TrajectoryExtractionConfig", | |
| "TrajectoryExtractor", | |
| "WorldHistoryProtocol", | |
| "extract_trajectory", | |
| ] |