diff --git a/desdeo/api/models/generic_states.py b/desdeo/api/models/generic_states.py index 26054a1ce..45b38bb34 100644 --- a/desdeo/api/models/generic_states.py +++ b/desdeo/api/models/generic_states.py @@ -17,9 +17,7 @@ from desdeo.problem import Tensor, VariableType -from .nautilus import ( - NautilusState, -) +from .nautilus import NautilusNavigatorInitializationState, NautilusNavigatorNavigationState from .state import ( EMOFetchState, EMOIterateState, @@ -160,63 +158,6 @@ def create( return row - # @classmethod - # def create( - # cls, - # database_session: Session, - # *, - # kind: StateKind, - # problem_id: int | None = None, - # session_id: int | None = None, - # parent_id: int | None = None, - # state: SQLModel | None = None, - # ) -> "StateDB": - # """Create a new StateDB entry and corresponding substate entry. - - # Args: - # database_session: Database session - # kind: Explicit StateKind of this state. - # problem_id: Required for root states. - # session_id: Required for child states. - # parent_id: Parent state ID if this is a child. - # state: The substate SQLModel instance. - # """ - # if state is None: - # raise ValueError("State (substate) must be provided.") - - # if parent_id is None: - # # Root state - # if problem_id is None: - # raise ValueError("Root state requires problem_id.") - # state_db = cls( - # problem_id=problem_id, - # session_id=None, - # parent_id=None, - # kind=kind, - # ) - # else: - # # Child state - # if session_id is None: - # raise ValueError("Child state requires session_id.") - # state_db = cls( - # problem_id=None, - # session_id=session_id, - # parent_id=parent_id, - # kind=kind, - # ) - - # database_session.add(state_db) - # database_session.commit() - # database_session.refresh(state_db) - - # # Link substate - # state.state_id = state_db.id - # database_session.add(state) - # database_session.commit() - # database_session.refresh(state_db) - - # return state_db - @property def state(self) -> SQLModel | None: """Return the concrete substate instance (e.g., NIMBUSSaveState)... @@ -256,8 +197,8 @@ def state(self) -> SQLModel | None: StateKind.GENERIC_INTERMEDIATE: IntermediateSolutionState, StateKind.ENAUTILUS_STEP: ENautilusState, StateKind.ENAUTILUS_FINAL: ENautilusFinalState, - StateKind.NAUTILUS_NAVIGATE: NautilusState, - StateKind.NAUTILUS_INITIALIZE: NautilusState + StateKind.NAUTILUS_NAVIGATE: NautilusNavigatorNavigationState, + StateKind.NAUTILUS_INITIALIZE: NautilusNavigatorInitializationState } SUBSTATE_TO_KIND: dict[SQLModel, StateKind] = { @@ -276,8 +217,8 @@ def state(self) -> SQLModel | None: IntermediateSolutionState: StateKind.GENERIC_INTERMEDIATE, ENautilusState: StateKind.ENAUTILUS_STEP, ENautilusFinalState: StateKind.ENAUTILUS_FINAL, - # NautilusState: StateKind.NAUTILUS_NAVIGATE, - # NautilusState: StateKind.NAUTILUS_INITIALIZE + NautilusNavigatorNavigationState: StateKind.NAUTILUS_NAVIGATE, + NautilusNavigatorInitializationState: StateKind.NAUTILUS_INITIALIZE } diff --git a/desdeo/api/models/nautilus.py b/desdeo/api/models/nautilus.py index 4db5e6281..80ec8abf4 100644 --- a/desdeo/api/models/nautilus.py +++ b/desdeo/api/models/nautilus.py @@ -1,43 +1,46 @@ from sqlmodel import JSON, Column, Field, SQLModel -class NautilusState(SQLModel, table=True): - """Concrete NAUTILUS Navigator state stored for a single interaction step. +class NautilusNavigatorInitializationState(SQLModel, table=True): + """State storing the inputs and outputs of the NAUTILUS Navigator initialization. - This model stores the full algorithmic state returned by the - NAUTILUS Navigator after either initialization or a navigation step. + This state corresponds to the execution of the `navigator_init` function in the + NAUTILUS Navigator core algorithm. It stores both the request provided by the + user and the resulting initialization information returned by the algorithm. - The instance is linked to a base `StateDB` entry, which defines the - problem context and state type (e.g. "nautilus.initialize" or - "nautilus.navigate"). This table contains only the algorithm-specific - data required to reconstruct the navigation process at that step. + The state is linked to a base `StateDB` entry which defines the interaction + type (`StateKind.NAUTILUS_INITIALIZE`) and stores the session hierarchy. + + The purpose of storing this information is to allow the API to: + 1. Retrieve previously computed initialization results without re-running + the algorithm. + 2. Reconstruct the algorithm state if the function must be re-evaluated. Attributes: - id (int | None): Primary key of this NAUTILUS state entry. + state_id (int): Foreign key referencing the base `StateDB` entry. + + request (dict): Serialized request data passed to `navigator_init`. + response (dict): Serialized response returned by `navigator_init`. + objective_symbols (list[str]): Short symbolic names of the objectives. objective_long_names (list[str]): Descriptive names of the objectives. - units (list[str] | None): Units of the objectives, if defined. None if unitless. + units (list[str] | None): Units of the objectives if defined, otherwise None. is_maximized (list[bool]): Boolean flags indicating whether each objective is to be maximized (True) or minimized (False). + ideal (list[float]): Ideal objective values of the problem. nadir (list[float]): Nadir objective values of the problem. - lower_bounds (dict[str, list[float]]): Lower bounds of the reachable region per objective - across navigation steps. - upper_bounds (dict[str, list[float]]): Upper bounds of the reachable region per objective - across navigation steps. - preferences (dict[str, list[float]]): Preference values provided by the decision maker - for each navigation step. - bounds (dict[str, list[float]]): Bound preferences provided by the decision maker - for each navigation step. - total_steps (int): Total number of steps allowed in this NAUTILUS session. - current_step (int): Current navigation step index. - remaining_steps (int): Number of steps remaining in the navigation process. - reachable_solution (dict[str, float]): The objective values of the currently reachable solution. + + total_steps (int): Total number of navigation steps specified for the session. """ - __tablename__ = "nautilus_states" + __tablename__ = "nautilus_navigator_initialization_states" - id: int | None = Field(default=None, primary_key=True) + state_id: int = Field(foreign_key="state.id", primary_key=True) + + # Stored request/response + request: dict = Field(sa_column=Column(JSON)) + response: dict = Field(sa_column=Column(JSON)) # Problem meta objective_symbols: list[str] = Field(sa_column=Column(JSON)) @@ -49,15 +52,69 @@ class NautilusState(SQLModel, table=True): ideal: list[float] = Field(sa_column=Column(JSON)) nadir: list[float] = Field(sa_column=Column(JSON)) - # Navigation data - lower_bounds: dict[str, list[float]] = Field(sa_column=Column(JSON)) - upper_bounds: dict[str, list[float]] = Field(sa_column=Column(JSON)) - preferences: dict[str, list[float]] = Field(sa_column=Column(JSON)) - bounds: dict[str, list[float]] = Field(sa_column=Column(JSON)) - + # Navigator configuration total_steps: int + + +class NautilusNavigatorNavigationState(SQLModel, table=True): + """State storing the inputs and outputs of a NAUTILUS Navigator navigation step. + + This state corresponds to the execution of the `navigator_all_steps` function + in the NAUTILUS Navigator algorithm. Each navigation step produces a new + reachable solution and updated bounds based on the decision maker's + preferences. + + The state stores both the user input and the algorithm output so that: + 1. The navigation history can be inspected without recomputing results. + 2. The algorithm can be re-evaluated if needed. + + The state is linked to a base `StateDB` entry which defines the interaction + type (`StateKind.NAUTILUS_NAVIGATE`) and the parent state relationship. + + Attributes: + state_id (int): Foreign key referencing the base `StateDB` entry. + + request (dict): Serialized navigation request provided by the decision maker. + response (dict): Serialized response returned by the navigator algorithm. + + current_step (int): Current step index in the navigation process. + remaining_steps (int): Number of remaining navigation steps. + + preferences (dict[str, list[float]]): Preference values provided by the + decision maker for each objective. + + bounds (dict[str, list[float]]): Bound preferences provided by the + decision maker. + + lower_bounds (dict[str, list[float]]): Lower bounds of the reachable + objective region after the navigation step. + + upper_bounds (dict[str, list[float]]): Upper bounds of the reachable + objective region after the navigation step. + + reachable_solution (dict[str, float]): Objective values of the currently + reachable solution produced by the navigation step. + """ + + __tablename__ = "nautilus_navigator_navigation_states" + + state_id: int = Field(foreign_key="state.id", primary_key=True) + + # Stored request/response + request: dict = Field(sa_column=Column(JSON)) + response: dict = Field(sa_column=Column(JSON)) + + # Navigation progress current_step: int remaining_steps: int - # Final reachable solution + # Decision maker input + preferences: dict[str, list[float]] = Field(sa_column=Column(JSON)) + bounds: dict[str, list[float]] = Field(sa_column=Column(JSON)) + + # Reachable region bounds + lower_bounds: dict[str, list[float]] = Field(sa_column=Column(JSON)) + upper_bounds: dict[str, list[float]] = Field(sa_column=Column(JSON)) + + # Resulting solution reachable_solution: dict[str, float] = Field(sa_column=Column(JSON))