Skip to content

Commit

Permalink
feat(robot-server): add runtime parameter definitions to run summary (#…
Browse files Browse the repository at this point in the history
…14866)

Adds the runtime parameter definitions to the run summary for both current and non current runs, accessible via the GET /runs and /runs/{run_id} endpoints.
  • Loading branch information
jbleon95 authored Apr 12, 2024
1 parent 044b37f commit b358ebe
Show file tree
Hide file tree
Showing 16 changed files with 509 additions and 14 deletions.
6 changes: 6 additions & 0 deletions robot-server/robot_server/persistence/_migrations/v3_to_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Summary of changes from schema 3:
- Adds a new "run_time_parameter_values_and_defaults" column to analysis table
- Adds a new "run_time_parameters" column to run table
"""

from pathlib import Path
Expand Down Expand Up @@ -50,3 +51,8 @@ def add_column(
schema_4.analysis_table.name,
schema_4.analysis_table.c.run_time_parameter_values_and_defaults,
)
add_column(
dest_engine,
schema_4.run_table.name,
schema_4.run_table.c.run_time_parameters,
)
19 changes: 15 additions & 4 deletions robot-server/robot_server/persistence/pydantic.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Store Pydantic objects in the SQL database."""

from typing import Type, TypeVar
from pydantic import BaseModel, parse_raw_as
import json
from typing import Type, TypeVar, List, Sequence
from pydantic import BaseModel, parse_raw_as, parse_obj_as


_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
Expand All @@ -17,6 +18,16 @@ def pydantic_to_json(obj: BaseModel) -> str:
)


def json_to_pydantic(model: Type[_BaseModelT], json: str) -> _BaseModelT:
def pydantic_list_to_json(obj_list: Sequence[BaseModel]) -> str:
"""Serialize a list of Pydantic objects for storing in the SQL database."""
return json.dumps([obj.dict(by_alias=True, exclude_none=True) for obj in obj_list])


def json_to_pydantic(model: Type[_BaseModelT], json_str: str) -> _BaseModelT:
"""Parse a Pydantic object stored in the SQL database."""
return parse_raw_as(model, json)
return parse_raw_as(model, json_str)


def json_to_pydantic_list(model: Type[_BaseModelT], json_str: str) -> List[_BaseModelT]:
"""Parse a list of Pydantic objects stored in the SQL database."""
return [parse_obj_as(model, obj_dict) for obj_dict in json.loads(json_str)]
7 changes: 7 additions & 0 deletions robot-server/robot_server/persistence/tables/schema_4.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
sqlalchemy.Column("engine_status", sqlalchemy.String, nullable=True),
# column added in schema v1
sqlalchemy.Column("_updated_at", UTCDateTime, nullable=True),
# column added in schema v4
sqlalchemy.Column(
"run_time_parameters",
# Stores a JSON string. See RunStore.
sqlalchemy.String,
nullable=True,
),
)

action_table = sqlalchemy.Table(
Expand Down
1 change: 1 addition & 0 deletions robot-server/robot_server/runs/run_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,5 @@ async def _run_protocol_and_insert_result(
run_id=self._run_id,
summary=result.state_summary,
commands=result.commands,
run_time_parameters=result.parameters,
)
20 changes: 18 additions & 2 deletions robot-server/robot_server/runs/run_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
from .run_store import RunResource, RunStore, BadRunResource, BadStateSummary
from .run_models import Run, BadRun, RunDataError

from opentrons.protocol_engine.types import DeckConfigurationType
from opentrons.protocol_engine.types import DeckConfigurationType, RunTimeParameter


def _build_run(
run_resource: Union[RunResource, BadRunResource],
state_summary: Union[StateSummary, BadStateSummary],
current: bool,
run_time_parameters: List[RunTimeParameter],
) -> Union[Run, BadRun]:
# TODO(mc, 2022-05-16): improve persistence strategy
# such that this default summary object is not needed
Expand All @@ -49,6 +50,7 @@ def _build_run(
completedAt=state_summary.completedAt,
startedAt=state_summary.startedAt,
liquids=state_summary.liquids,
runTimeParameters=run_time_parameters,
)

errors: List[EnumeratedError] = []
Expand Down Expand Up @@ -102,6 +104,7 @@ def _build_run(
completedAt=state.completedAt,
startedAt=state.startedAt,
liquids=state.liquids,
runTimeParameters=run_time_parameters,
)


Expand Down Expand Up @@ -172,6 +175,7 @@ async def create(
run_id=prev_run_id,
summary=prev_run_result.state_summary,
commands=prev_run_result.commands,
run_time_parameters=prev_run_result.parameters,
)
state_summary = await self._engine_store.create(
run_id=run_id,
Expand All @@ -196,6 +200,7 @@ async def create(
run_resource=run_resource,
state_summary=state_summary,
current=True,
run_time_parameters=[],
)

def get(self, run_id: str) -> Union[Run, BadRun]:
Expand All @@ -215,9 +220,10 @@ def get(self, run_id: str) -> Union[Run, BadRun]:
"""
run_resource = self._run_store.get(run_id=run_id)
state_summary = self._get_state_summary(run_id=run_id)
parameters = self._get_run_time_parameters(run_id=run_id)
current = run_id == self._engine_store.current_run_id

return _build_run(run_resource, state_summary, current)
return _build_run(run_resource, state_summary, current, parameters)

def get_run_loaded_labware_definitions(
self, run_id: str
Expand Down Expand Up @@ -260,6 +266,7 @@ def get_all(self, length: Optional[int]) -> List[Union[Run, BadRun]]:
run_resource=run_resource,
state_summary=self._get_state_summary(run_resource.run_id),
current=run_resource.run_id == self._engine_store.current_run_id,
run_time_parameters=self._get_run_time_parameters(run_resource.run_id),
)
for run_resource in self._run_store.get_all(length)
]
Expand Down Expand Up @@ -310,15 +317,18 @@ async def update(self, run_id: str, current: Optional[bool]) -> Union[Run, BadRu
run_id=run_id,
summary=state_summary,
commands=commands,
run_time_parameters=parameters,
)
else:
state_summary = self._engine_store.engine.state_view.get_summary()
parameters = self._engine_store.runner.run_time_parameters
run_resource = self._run_store.get(run_id=run_id)

return _build_run(
run_resource=run_resource,
state_summary=state_summary,
current=next_current,
run_time_parameters=parameters,
)

def get_commands_slice(
Expand Down Expand Up @@ -385,3 +395,9 @@ def _get_state_summary(self, run_id: str) -> Union[StateSummary, BadStateSummary
def _get_good_state_summary(self, run_id: str) -> Optional[StateSummary]:
summary = self._get_state_summary(run_id)
return summary if isinstance(summary, StateSummary) else None

def _get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]:
if run_id == self._engine_store.current_run_id:
return self._engine_store.runner.run_time_parameters
else:
return self._run_store.get_run_time_parameters(run_id=run_id)
20 changes: 19 additions & 1 deletion robot-server/robot_server/runs/run_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Liquid,
CommandNote,
)
from opentrons.protocol_engine.types import RunTimeParamValuesType
from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType
from opentrons_shared_data.errors import GeneralError
from robot_server.service.json_api import ResourceModel
from robot_server.errors.error_responses import ErrorDetails
Expand Down Expand Up @@ -121,6 +121,15 @@ class Run(ResourceModel):
...,
description="Labware offsets to apply as labware are loaded.",
)
runTimeParameters: List[RunTimeParameter] = Field(
default_factory=list,
description=(
"Run time parameters used during the run."
" These are the parameters that are defined in the protocol, with values"
" specified either in the run creation request or default values from the protocol"
" if none are specified in the request."
),
)
protocolId: Optional[str] = Field(
None,
description=(
Expand Down Expand Up @@ -185,6 +194,15 @@ class BadRun(ResourceModel):
...,
description="Labware offsets to apply as labware are loaded.",
)
runTimeParameters: List[RunTimeParameter] = Field(
default_factory=list,
description=(
"Run time parameters used during the run."
" These are the parameters that are defined in the protocol, with values"
" specified either in the run creation request or default values from the protocol"
" if none are specified in the request."
),
)
protocolId: Optional[str] = Field(
None,
description=(
Expand Down
41 changes: 40 additions & 1 deletion robot-server/robot_server/runs/run_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from opentrons.util.helpers import utc_now
from opentrons.protocol_engine import StateSummary, CommandSlice
from opentrons.protocol_engine.commands import Command
from opentrons.protocol_engine.types import RunTimeParameter

from opentrons_shared_data.errors.exceptions import (
EnumeratedError,
Expand All @@ -25,7 +26,12 @@
run_command_table,
action_table,
)
from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json
from robot_server.persistence.pydantic import (
json_to_pydantic,
pydantic_to_json,
json_to_pydantic_list,
pydantic_list_to_json,
)
from robot_server.protocols.protocol_store import ProtocolNotFoundError

from .action_models import RunAction, RunActionType
Expand Down Expand Up @@ -102,13 +108,15 @@ def update_run_state(
run_id: str,
summary: StateSummary,
commands: List[Command],
run_time_parameters: List[RunTimeParameter],
) -> RunResource:
"""Update the run's state summary and commands list.
Args:
run_id: The run to update
summary: The run's equipment and status summary.
commands: The run's commands.
run_time_parameters: The run's run time parameters, if any.
Returns:
The run resource.
Expand All @@ -124,6 +132,7 @@ def update_run_state(
run_id=run_id,
state_summary=summary,
engine_status=summary.status,
run_time_parameters=run_time_parameters,
)
)
)
Expand Down Expand Up @@ -346,6 +355,33 @@ def get_state_summary(self, run_id: str) -> Union[StateSummary, BadStateSummary]
)
)

@lru_cache(maxsize=_CACHE_ENTRIES)
def get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]:
"""Get the archived run time parameters.
This is a list of the run's parameter definitions (if any),
including the values used in the run itself, along with the default value,
constraints and associated names and descriptions.
"""
select_run_data = sqlalchemy.select(run_table.c.run_time_parameters).where(
run_table.c.id == run_id
)

with self._sql_engine.begin() as transaction:
row = transaction.execute(select_run_data).one()

try:
return (
json_to_pydantic_list(RunTimeParameter, row.run_time_parameters) # type: ignore[arg-type]
if row.run_time_parameters is not None
else []
)
except ValidationError:
log.warning(
f"Error retrieving run time parameters for {run_id}", exc_info=True
)
return []

def get_commands_slice(
self,
run_id: str,
Expand Down Expand Up @@ -476,6 +512,7 @@ def _clear_caches(self) -> None:
self.get_all.cache_clear()
self.get_state_summary.cache_clear()
self.get_command.cache_clear()
self.get_run_time_parameters.cache_clear()


# The columns that must be present in a row passed to _convert_row_to_run().
Expand Down Expand Up @@ -552,9 +589,11 @@ def _convert_state_to_sql_values(
run_id: str,
state_summary: StateSummary,
engine_status: str,
run_time_parameters: List[RunTimeParameter],
) -> Dict[str, object]:
return {
"state_summary": pydantic_to_json(state_summary),
"engine_status": engine_status,
"_updated_at": utc_now(),
"run_time_parameters": pydantic_list_to_json(run_time_parameters),
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ stages:
displayName: Water
description: Liquid H2O
displayColor: '#7332a8'
runTimeParameters: []
protocolId: '{protocol_id}'

- name: Execute a setup command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ stages:
definitionUri: opentrons/opentrons_1_trash_1100ml_fixed/1
location: !anydict
labwareOffsets: []
runTimeParameters: []
liquids:
- id: waterId
displayName: Water
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ stages:
definitionUri: opentrons/opentrons_1_trash_1100ml_fixed/1
location: !anydict
labwareOffsets: []
runTimeParameters: []
protocolId: '{protocol_id}'
liquids: []
save:
Expand Down Expand Up @@ -237,6 +238,7 @@ stages:
createdAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$"
startedAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$"
liquids: []
runTimeParameters: []
completedAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$"
errors: []
pipettes: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ stages:
labware: []
labwareOffsets: []
liquids: []
runTimeParameters: []
modules: []
pipettes: []
status: 'idle'
Expand Down
Loading

0 comments on commit b358ebe

Please sign in to comment.