From d2a96af2332cd68d556ccbab227baa4d11025a2a Mon Sep 17 00:00:00 2001 From: Peter Bednarik Date: Wed, 4 Mar 2026 10:33:18 +0200 Subject: [PATCH 1/2] StateDB used, node_type in schemas removed, generic_states additions but question as well. --- desdeo/api/models/generic_states.py | 144 ++++++++++++++++++++-------- desdeo/api/models/nautilus.py | 79 +++++++++------ desdeo/api/schemas/nautilus.py | 4 +- 3 files changed, 154 insertions(+), 73 deletions(-) diff --git a/desdeo/api/models/generic_states.py b/desdeo/api/models/generic_states.py index 7baeb2a81..63df735ff 100644 --- a/desdeo/api/models/generic_states.py +++ b/desdeo/api/models/generic_states.py @@ -28,6 +28,7 @@ GNIMBUSOptimizationState, GNIMBUSVotingState, IntermediateSolutionState, + NautilusState, NIMBUSClassificationState, NIMBUSFinalState, NIMBUSInitializationState, @@ -64,6 +65,8 @@ class StateKind(str, Enum): GENERIC_INTERMEDIATE = "generic.solve_intermediate" ENAUTILUS_STEP = "e-nautilus.stepping" ENAUTILUS_FINAL = "e-nautilus.final" + NAUTILUS_NAVIGATE = "nautilus.navigate" + NAUTILUS_INITIALIZE = "nautilus.initialize" class State(SQLModel, table=True): @@ -117,43 +120,100 @@ class StateDB(SQLModel, table=True): session: "InteractiveSessionDB" = Relationship(back_populates="states") problem: "ProblemDB" = Relationship(back_populates="states") + # @classmethod + # def create( + # cls, + # database_session: Session, + # *, + # problem_id: int | None = None, + # session_id: int | None = None, + # parent_id: int | None = None, + # state: SQLModel | None = None, + # ) -> "StateDB": + # """Build a StateDB + base State with a concrete substate.""" + # sub_cls = type(state) + # kind: StateKind | None = None + + # for cls_in_mro in sub_cls.mro(): + # if cls_in_mro in SUBSTATE_TO_KIND: + # kind = SUBSTATE_TO_KIND[cls_in_mro] + # break + + # if kind is None: + # raise ValueError(f"No StateKind mapping for substate type {sub_cls!r}") + + # method, phase = _method_phase_from_kind(kind) + # base = State(method=method, phase=phase, kind=kind) + + # row = cls( + # problem_id=problem_id, + # session_id=session_id, + # parent_id=parent_id, + # base_state=base, + # ) + # database_session.add(row) + + # # Persist base and link substate PK=FK + # _attach_substate(database_session, base, state) + + # 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": - """Build a StateDB + base State with a concrete substate.""" - sub_cls = type(state) - kind: StateKind | None = None - - for cls_in_mro in sub_cls.mro(): - if cls_in_mro in SUBSTATE_TO_KIND: - kind = SUBSTATE_TO_KIND[cls_in_mro] - break - - if kind is None: - raise ValueError(f"No StateKind mapping for substate type {sub_cls!r}") + """Create a new StateDB entry and corresponding substate entry. - method, phase = _method_phase_from_kind(kind) - base = State(method=method, phase=phase, kind=kind) + 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, + ) - row = cls( - problem_id=problem_id, - session_id=session_id, - parent_id=parent_id, - base_state=base, - ) - database_session.add(row) + database_session.add(state_db) + database_session.commit() + database_session.refresh(state_db) - # Persist base and link substate PK=FK - _attach_substate(database_session, base, state) + # Link substate + state.state_id = state_db.id + database_session.add(state) + database_session.commit() + database_session.refresh(state_db) - return row + return state_db @property def state(self) -> SQLModel | None: @@ -194,25 +254,29 @@ 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 } -SUBSTATE_TO_KIND: dict[SQLModel, StateKind] = { - RPMState: StateKind.RPM_SOLVE, - NIMBUSClassificationState: StateKind.NIMBUS_SOLVE, - NIMBUSSaveState: StateKind.NIMBUS_SAVE, - NIMBUSInitializationState: StateKind.NIMBUS_INIT, - NIMBUSFinalState: StateKind.NIMBUS_FINAL, - EMOIterateState: StateKind.EMO_RUN, - GNIMBUSOptimizationState: StateKind.GNIMBUS_OPTIMIZE, - GNIMBUSVotingState: StateKind.GNIMBUS_VOTE, - GNIMBUSEndState: StateKind.GNIMBUS_END, - EMOSaveState: StateKind.EMO_SAVE, - EMOFetchState: StateKind.EMO_FETCH, - EMOSCOREState: StateKind.EMO_SCORE, - IntermediateSolutionState: StateKind.GENERIC_INTERMEDIATE, - ENautilusState: StateKind.ENAUTILUS_STEP, - ENautilusFinalState: StateKind.ENAUTILUS_FINAL, -} +# SUBSTATE_TO_KIND: dict[SQLModel, StateKind] = { +# RPMState: StateKind.RPM_SOLVE, +# NIMBUSClassificationState: StateKind.NIMBUS_SOLVE, +# NIMBUSSaveState: StateKind.NIMBUS_SAVE, +# NIMBUSInitializationState: StateKind.NIMBUS_INIT, +# NIMBUSFinalState: StateKind.NIMBUS_FINAL, +# EMOIterateState: StateKind.EMO_RUN, +# GNIMBUSOptimizationState: StateKind.GNIMBUS_OPTIMIZE, +# GNIMBUSVotingState: StateKind.GNIMBUS_VOTE, +# GNIMBUSEndState: StateKind.GNIMBUS_END, +# EMOSaveState: StateKind.EMO_SAVE, +# EMOFetchState: StateKind.EMO_FETCH, +# EMOSCOREState: StateKind.EMO_SCORE, +# IntermediateSolutionState: StateKind.GENERIC_INTERMEDIATE, +# ENautilusState: StateKind.ENAUTILUS_STEP, +# ENautilusFinalState: StateKind.ENAUTILUS_FINAL, +# # NautilusState: StateKind.NAUTILUS_NAVIGATE, +# # NautilusState: StateKind.NAUTILUS_INITIALIZE +# } def _method_phase_from_kind(kind: StateKind) -> tuple[str, str]: diff --git a/desdeo/api/models/nautilus.py b/desdeo/api/models/nautilus.py index 027697050..4db5e6281 100644 --- a/desdeo/api/models/nautilus.py +++ b/desdeo/api/models/nautilus.py @@ -1,44 +1,63 @@ -import datetime +from sqlmodel import JSON, Column, Field, SQLModel -from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String -from desdeo.api.db import Base +class NautilusState(SQLModel, table=True): + """Concrete NAUTILUS Navigator state stored for a single interaction step. + This model stores the full algorithmic state returned by the + NAUTILUS Navigator after either initialization or a navigation step. -class NautilusStateDB(Base): - """Database model storing the state of a NAUTILUS interactive navigation session. - - Each record represents a single interaction step within a NAUTILUS session, - such as initialization, navigation, or finalization. The table supports - branching session trees by maintaining parent-child relationships between states. + 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. Attributes: - id (int): Primary key identifying this state entry. - session_id (int): Identifier of the interactive NAUTILUS session. - parent_state_id (int | None): Foreign key referencing the parent state. - Null for root (initialization) nodes. - request (JSON): Serialized Nautilus request model used to generate this state. - response (JSON): Serialized Nautilus response model produced by the algorithm. - node_type (str): Type of interaction that generated the state. - Expected values include: - - "initialize" - - "navigate" - - "final" - depth (int): Depth of this node within the session tree (root = 0). - created_at (datetime): Timestamp when this state was created. + id (int | None): Primary key of this NAUTILUS state entry. + 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. + 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. """ __tablename__ = "nautilus_states" - id = Column(Integer, primary_key=True) - session_id = Column(Integer, nullable=False) - parent_state_id = Column(Integer, ForeignKey("nautilus_states.id"), nullable=True) + id: int | None = Field(default=None, primary_key=True) + + # Problem meta + objective_symbols: list[str] = Field(sa_column=Column(JSON)) + objective_long_names: list[str] = Field(sa_column=Column(JSON)) + units: list[str] | None = Field(default=None, sa_column=Column(JSON)) + is_maximized: list[bool] = Field(sa_column=Column(JSON)) - request = Column(JSON, nullable=False) - response = Column(JSON, nullable=False) + # Problem bounds + ideal: list[float] = Field(sa_column=Column(JSON)) + nadir: list[float] = Field(sa_column=Column(JSON)) - node_type = Column(String, nullable=False) # "initialize", "navigate", "final" + # 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)) - depth = Column(Integer, nullable=False) + total_steps: int + current_step: int + remaining_steps: int - created_at = Column(DateTime, default=datetime.utcnow) + # Final reachable solution + reachable_solution: dict[str, float] = Field(sa_column=Column(JSON)) diff --git a/desdeo/api/schemas/nautilus.py b/desdeo/api/schemas/nautilus.py index 90299857d..49543fa7d 100644 --- a/desdeo/api/schemas/nautilus.py +++ b/desdeo/api/schemas/nautilus.py @@ -25,7 +25,6 @@ class NautilusInitialResponse(BaseModel): state_id: int = Field(..., description="The ID of this navigation state.") response_type: Literal["nautilus.initialize"] = "nautilus.initialize" - node_type: Literal["initialize"] = "initialize" parent_state_id: int | None = Field(None, description="Parent state ID, if this is a child step.") objective_symbols: list[str] = Field(..., description="The symbols of the objectives.") @@ -42,7 +41,6 @@ class NautilusNavigateResponse(BaseModel): state_id: int = Field(..., description="The ID of this navigation state.") response_type: Literal["nautilus.navigate"] = "nautilus.navigate" - node_type: Literal["navigate", "final"] = "navigate" parent_state_id: int | None = Field(None, description="Parent state ID, if this is a child step.") objective_symbols: list[str] = Field(..., description="The symbols of the objectives.") @@ -58,4 +56,4 @@ class NautilusNavigateResponse(BaseModel): bounds: dict[str, list[float]] = Field(..., description="Bounds used in each step per objective.") total_steps: int = Field(..., description="The total number of steps in the current navigation path.") - reachable_solution: dict = Field(..., description="The solution reached at the end of navigation.") \ No newline at end of file + reachable_solution: dict[str, float] = Field(..., description="The solution reached at the end of navigation.") From 654f27d171ee6dae475822c5add2127e45774023 Mon Sep 17 00:00:00 2001 From: Peter Bednarik Date: Wed, 4 Mar 2026 10:43:36 +0200 Subject: [PATCH 2/2] Repair, new version disabled. --- desdeo/api/models/generic_states.py | 202 ++++++++++++++-------------- 1 file changed, 102 insertions(+), 100 deletions(-) diff --git a/desdeo/api/models/generic_states.py b/desdeo/api/models/generic_states.py index 63df735ff..26054a1ce 100644 --- a/desdeo/api/models/generic_states.py +++ b/desdeo/api/models/generic_states.py @@ -17,6 +17,9 @@ from desdeo.problem import Tensor, VariableType +from .nautilus import ( + NautilusState, +) from .state import ( EMOFetchState, EMOIterateState, @@ -28,7 +31,6 @@ GNIMBUSOptimizationState, GNIMBUSVotingState, IntermediateSolutionState, - NautilusState, NIMBUSClassificationState, NIMBUSFinalState, NIMBUSInitializationState, @@ -120,100 +122,100 @@ class StateDB(SQLModel, table=True): session: "InteractiveSessionDB" = Relationship(back_populates="states") problem: "ProblemDB" = Relationship(back_populates="states") - # @classmethod - # def create( - # cls, - # database_session: Session, - # *, - # problem_id: int | None = None, - # session_id: int | None = None, - # parent_id: int | None = None, - # state: SQLModel | None = None, - # ) -> "StateDB": - # """Build a StateDB + base State with a concrete substate.""" - # sub_cls = type(state) - # kind: StateKind | None = None - - # for cls_in_mro in sub_cls.mro(): - # if cls_in_mro in SUBSTATE_TO_KIND: - # kind = SUBSTATE_TO_KIND[cls_in_mro] - # break - - # if kind is None: - # raise ValueError(f"No StateKind mapping for substate type {sub_cls!r}") - - # method, phase = _method_phase_from_kind(kind) - # base = State(method=method, phase=phase, kind=kind) - - # row = cls( - # problem_id=problem_id, - # session_id=session_id, - # parent_id=parent_id, - # base_state=base, - # ) - # database_session.add(row) - - # # Persist base and link substate PK=FK - # _attach_substate(database_session, base, state) - - # 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. + """Build a StateDB + base State with a concrete substate.""" + sub_cls = type(state) + kind: StateKind | None = None - 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, - ) + for cls_in_mro in sub_cls.mro(): + if cls_in_mro in SUBSTATE_TO_KIND: + kind = SUBSTATE_TO_KIND[cls_in_mro] + break + + if kind is None: + raise ValueError(f"No StateKind mapping for substate type {sub_cls!r}") - database_session.add(state_db) - database_session.commit() - database_session.refresh(state_db) + method, phase = _method_phase_from_kind(kind) + base = State(method=method, phase=phase, kind=kind) - # Link substate - state.state_id = state_db.id - database_session.add(state) - database_session.commit() - database_session.refresh(state_db) + row = cls( + problem_id=problem_id, + session_id=session_id, + parent_id=parent_id, + base_state=base, + ) + database_session.add(row) + + # Persist base and link substate PK=FK + _attach_substate(database_session, base, state) - return state_db + 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: @@ -258,25 +260,25 @@ def state(self) -> SQLModel | None: StateKind.NAUTILUS_INITIALIZE: NautilusState } -# SUBSTATE_TO_KIND: dict[SQLModel, StateKind] = { -# RPMState: StateKind.RPM_SOLVE, -# NIMBUSClassificationState: StateKind.NIMBUS_SOLVE, -# NIMBUSSaveState: StateKind.NIMBUS_SAVE, -# NIMBUSInitializationState: StateKind.NIMBUS_INIT, -# NIMBUSFinalState: StateKind.NIMBUS_FINAL, -# EMOIterateState: StateKind.EMO_RUN, -# GNIMBUSOptimizationState: StateKind.GNIMBUS_OPTIMIZE, -# GNIMBUSVotingState: StateKind.GNIMBUS_VOTE, -# GNIMBUSEndState: StateKind.GNIMBUS_END, -# EMOSaveState: StateKind.EMO_SAVE, -# EMOFetchState: StateKind.EMO_FETCH, -# EMOSCOREState: StateKind.EMO_SCORE, -# IntermediateSolutionState: StateKind.GENERIC_INTERMEDIATE, -# ENautilusState: StateKind.ENAUTILUS_STEP, -# ENautilusFinalState: StateKind.ENAUTILUS_FINAL, -# # NautilusState: StateKind.NAUTILUS_NAVIGATE, -# # NautilusState: StateKind.NAUTILUS_INITIALIZE -# } +SUBSTATE_TO_KIND: dict[SQLModel, StateKind] = { + RPMState: StateKind.RPM_SOLVE, + NIMBUSClassificationState: StateKind.NIMBUS_SOLVE, + NIMBUSSaveState: StateKind.NIMBUS_SAVE, + NIMBUSInitializationState: StateKind.NIMBUS_INIT, + NIMBUSFinalState: StateKind.NIMBUS_FINAL, + EMOIterateState: StateKind.EMO_RUN, + GNIMBUSOptimizationState: StateKind.GNIMBUS_OPTIMIZE, + GNIMBUSVotingState: StateKind.GNIMBUS_VOTE, + GNIMBUSEndState: StateKind.GNIMBUS_END, + EMOSaveState: StateKind.EMO_SAVE, + EMOFetchState: StateKind.EMO_FETCH, + EMOSCOREState: StateKind.EMO_SCORE, + IntermediateSolutionState: StateKind.GENERIC_INTERMEDIATE, + ENautilusState: StateKind.ENAUTILUS_STEP, + ENautilusFinalState: StateKind.ENAUTILUS_FINAL, + # NautilusState: StateKind.NAUTILUS_NAVIGATE, + # NautilusState: StateKind.NAUTILUS_INITIALIZE +} def _method_phase_from_kind(kind: StateKind) -> tuple[str, str]: