saealib.result のソースコード

"""Optimization result returned by the high-level API."""

from __future__ import annotations

from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

import numpy as np

from saealib.exceptions import ValidationError

if TYPE_CHECKING:
    from saealib.context import OptimizationState
    from saealib.execution.history import History


_HISTORY_X_NAMES = {
    "fe": "Function evaluations",
    "gen": "Generations",
    "decision_count": "Decisions",
}
_HISTORY_X = frozenset(_HISTORY_X_NAMES)


@dataclass(frozen=True)
class HistorySeries:
    """A history series and the names used to describe its coordinates.

    Parameters
    ----------
    x : np.ndarray
        Values for the independent coordinate.
    y : np.ndarray
        Values for the dependent coordinate.
    x_name : str
        Display name for the independent coordinate.
    y_name : str
        Display name for the dependent coordinate.
    """

    x: np.ndarray
    y: np.ndarray
    x_name: str
    y_name: str


[ドキュメント] @dataclass class Result: """Optimization result returned by :func:`minimize` / :func:`maximize`. ``x``/``f``/``fe``/``gen`` are fixed at creation time (the arrays are copied). ``ctx``/``history``/``archive``/``pareto_archive``/``population`` are references derived from the state and may change while iteration is still in progress. Attributes ---------- x : np.ndarray or None Best design variables. Shape ``(dim,)`` for single-objective, ``(n_pareto, dim)`` for multi-objective. ``None`` for non-dense spaces; use ``problem.space.services`` to convert ``result.ctx.archive.genomes`` when dense design variables are needed. f : np.ndarray Best objective values. Shape ``(n_obj,)`` for single-objective, ``(n_pareto, n_obj)`` for multi-objective. fe : int Total number of true function evaluations used. gen : int Total number of generations completed. history : History or None Execution history recorded during the run. ctx : OptimizationState Full optimization context providing access to the archive and more. """ x: np.ndarray | None f: np.ndarray fe: int gen: int ctx: OptimizationState history: History | None = None
[ドキュメント] @classmethod def from_state(cls, state: OptimizationState) -> Result: """Build a result from the final or currently observed state. Result feasibility uses ``problem.eps_cv`` and intentionally differs from ``problem.handler.feasibility_threshold``. """ archive_f = state.archive.get_array("f") archive_cv = state.archive.get_array("cv") direction = state.problem.direction eps = state.problem.eps_cv feasible = np.where(archive_cv <= eps)[0] pool = feasible if len(feasible) else np.array([int(np.argmin(archive_cv))]) if state.problem.n_obj == 1: scores = archive_f[pool] @ direction best_idx = pool[int(np.argmax(scores))] try: best_x = state.archive.get_array("x")[best_idx] except AttributeError: best_x = None best_f = archive_f[best_idx] else: if len(state.pareto_archive) > 0: try: best_x = state.pareto_archive.get_array("x") except AttributeError: best_x = None best_f = state.pareto_archive.get_array("f") else: from saealib.comparators import non_dominated_sort _, fronts = non_dominated_sort(archive_f[pool], direction=direction) pareto_idx = pool[fronts[0]] try: best_x = state.archive.get_array("x")[pareto_idx] except AttributeError: best_x = None best_f = archive_f[pareto_idx] return cls( x=None if best_x is None else np.array(best_x, copy=True), f=np.array(best_f, copy=True), fe=int(state.fe), gen=int(state.gen), history=state.history, ctx=state, )
@property def problem(self): """Return the problem referenced by the optimization state.""" return self.ctx.problem @property def archive(self): """Return the archive referenced by the optimization state.""" return self.ctx.archive @property def pareto_archive(self): """Return the Pareto archive referenced by the optimization state.""" return self.ctx.pareto_archive @property def population(self): """Return the population referenced by the optimization state.""" return self.ctx.population
[ドキュメント] def history_series( self, value: str | Callable[[Mapping[str, Any]], float], *, x: str = "fe", channel: str | None = None, **value_kwargs: Any, ) -> HistorySeries: """Return one scalar value per recorded history generation. Parameters ---------- value : str or callable Registered value name, or a callable mapping one history record to a scalar. A callable also requires *channel*. x : {"fe", "gen", "decision_count"}, optional Column supplying the independent coordinate. channel : str, optional History channel whose records feed a callable *value*. **value_kwargs Arguments required by the registered value: ``reference_point`` for ``"hypervolume"``, ``reference_front`` for ``"gd"``, ``"gd_plus"``, ``"igd"``, ``"igd_plus"`` and ``"spread"``. Returns ------- HistorySeries The series and the display names of its two coordinates. Raises ------ ValidationError If *x* or *value* is unknown, the channel the value needs was not recorded, or a reference argument has the wrong shape. Notes ----- A reference is supplied in the objective space of the problem, the same orientation as :attr:`Result.f`. Front and reference are both converted to minimize-space before an indicator runs, and neither is normalized, so an indicator value keeps raw objective units. Indicators read the ``front`` channel, a per-generation snapshot of the Pareto archive holding objective values alone, with no constraint violation column. That archive judges feasibility by its own ``eps_cv``, which the built-in algorithms leave at ``0.0``, so it admits a solution as feasible only at exactly zero violation. While it holds no such solution, constrained dominance reduces the front to the lowest-cv solutions and an indicator over that generation describes an infeasible front; ``front_size`` collapsing to a single point is the visible sign. Feasibility elsewhere in the series rests on different thresholds: ``"best"`` selects from the archive under ``problem.eps_cv``, and ``"feasible_ratio"`` counts the population under the running ``problem.handler.feasibility_threshold``. The three are independent and can disagree, most visibly while epsilon is relaxed. """ if x not in _HISTORY_X: valid = ", ".join(f'"{name}"' for name in _HISTORY_X_NAMES) raise ValidationError(f"x must be one of: {valid}.") if self.history is None: raise ValidationError( "history_series requires execution history, but the result has none. " "Record it by passing history_channels=[...] to minimize() or by " "enabling it on the Optimizer with set_history([...])." ) if isinstance(value, str): from saealib._series_values import _HISTORY_VALUES, _builtin_values try: spec = _HISTORY_VALUES[value] except KeyError as exc: valid = ", ".join(_HISTORY_VALUES) raise ValidationError( f"Unknown history value {value!r}. Choose one of: {valid}." ) from exc if not self.history.is_enabled(spec.channel): raise ValidationError( f'history_series value "{value}" requires the ' f'"{spec.channel}" history channel. ' "Enable it with minimize(..., history_channels=[...]) or " "Optimizer.set_history([...]), then rerun the optimization." ) y = _builtin_values(self, value, spec, value_kwargs) y_name = value elif callable(value): if channel is None: raise ValidationError( "A callable history value requires channel=... so its " "records can be selected." ) if not self.history.is_enabled(channel): raise ValidationError( f'history_series callable value requires the "{channel}" ' "history channel. Enable it with " "minimize(..., history_channels=[...]) or " "Optimizer.set_history([...]), then rerun the optimization." ) records = list(self.history.records(channel)) try: y = [float(value(record)) for record in records] x_values = [float(record[x]) for record in records] except KeyError as exc: raise ValidationError( f'history channel {channel!r} is missing the "{x}" column.' ) from exc except (TypeError, ValueError, OverflowError) as exc: raise ValidationError( "A callable history value must return a scalar number." ) from exc return HistorySeries( x=np.asarray(x_values, dtype=float), y=np.asarray(y, dtype=float), x_name=_HISTORY_X_NAMES[x], y_name=getattr(value, "__name__", "callable"), ) else: raise ValidationError("value must be a registered name or callable.") x_values = self.history.get(spec.channel, x) return HistorySeries( x=np.asarray(x_values, dtype=float).copy(), y=np.asarray(y, dtype=float), x_name=_HISTORY_X_NAMES[x], y_name=y_name, )