diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index c8b11f2db25..62a5e11edd5 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -67,6 +67,16 @@ class PendingAnalysis(BaseModel): AnalysisStatus.PENDING, description="Status marking the analysis as pending", ) + runTimeParameters: List[RunTimeParameter] = Field( + default_factory=list, + description=( + "Run time parameters used during analysis." + " These are the parameters that are defined in the protocol, with values" + " specified either in the protocol creation request or reanalysis request" + " (whichever started this analysis), or default values from the protocol" + " if none are specified in the request." + ), + ) class CompletedAnalysis(BaseModel): diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 4f5b66ed4f8..c48f3be3a07 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -116,7 +116,12 @@ def __init__( current_analyzer_version=_CURRENT_ANALYZER_VERSION, ) - def add_pending(self, protocol_id: str, analysis_id: str) -> AnalysisSummary: + def add_pending( + self, + protocol_id: str, + analysis_id: str, + run_time_parameters: Optional[List[RunTimeParameter]] = None, + ) -> AnalysisSummary: """Add a new pending analysis to the store. Args: @@ -125,12 +130,16 @@ def add_pending(self, protocol_id: str, analysis_id: str) -> AnalysisSummary: a pending analysis. analysis_id: The ID of the new analysis. Must be unique across *all* protocols, not just this one. + run_time_parameters: Run time parameters defined in the protocol, along with + override values for the run. Returns: A summary of the just-added analysis. """ new_pending_analysis = self._pending_store.add( - protocol_id=protocol_id, analysis_id=analysis_id + protocol_id=protocol_id, + analysis_id=analysis_id, + run_time_parameters=run_time_parameters or [], ) return _summarize_pending(pending_analysis=new_pending_analysis) @@ -371,13 +380,20 @@ def __init__(self) -> None: self._analysis_ids_by_protocol_id: Dict[str, str] = {} self._protocol_ids_by_analysis_id: Dict[str, str] = {} - def add(self, protocol_id: str, analysis_id: str) -> PendingAnalysis: + def add( + self, + protocol_id: str, + analysis_id: str, + run_time_parameters: List[RunTimeParameter], + ) -> PendingAnalysis: """Add a new pending analysis and associate it with the given protocol.""" assert ( protocol_id not in self._analysis_ids_by_protocol_id ), "Protocol must not already have a pending analysis." - new_pending_analysis = PendingAnalysis.construct(id=analysis_id) + new_pending_analysis = PendingAnalysis.construct( + id=analysis_id, runTimeParameters=run_time_parameters + ) self._analyses_by_id[analysis_id] = new_pending_analysis self._analysis_ids_by_protocol_id[protocol_id] = analysis_id diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index 1ecee27e1da..451b9889c31 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -1,11 +1,15 @@ """Protocol analysis module.""" import logging -from typing import Optional +from typing import Optional, List + +from opentrons_shared_data.robot.dev_types import RobotType from opentrons import protocol_runner from opentrons.protocol_engine.errors import ErrorOccurrence -from opentrons.protocol_engine.types import RunTimeParamValuesType +from opentrons.protocol_engine.types import RunTimeParamValuesType, RunTimeParameter import opentrons.util.helpers as datetime_helper +from opentrons.protocol_runner import AbstractRunner, PythonAndLegacyRunner, JsonRunner +from opentrons.protocols.parse import PythonParseMode import robot_server.errors.error_mappers as em @@ -24,46 +28,46 @@ def __init__( ) -> None: """Initialize the analyzer and its dependencies.""" self._analysis_store = analysis_store + self._runner: Optional[AbstractRunner] = None - async def analyze( + async def load_runner( self, protocol_resource: ProtocolResource, - analysis_id: str, run_time_param_values: Optional[RunTimeParamValuesType], - ) -> None: - """Analyze a given protocol, storing the analysis when complete.""" - runner = await protocol_runner.create_simulating_runner( + ) -> List[RunTimeParameter]: + """Load the runner with the protocl and run time parameters.""" + self._runner = await protocol_runner.create_simulating_runner( robot_type=protocol_resource.source.robot_type, protocol_config=protocol_resource.source.config, ) - try: - result = await runner.run( + if isinstance(self._runner, PythonAndLegacyRunner): + await self._runner.load( protocol_source=protocol_resource.source, - deck_configuration=[], + python_parse_mode=PythonParseMode.NORMAL, run_time_param_values=run_time_param_values, ) + elif isinstance(self._runner, JsonRunner): + await self._runner.load(protocol_source=protocol_resource.source) + return self._runner.run_time_parameters + + async def analyze( + self, + protocol_resource: ProtocolResource, + analysis_id: str, + run_time_parameters: Optional[List[RunTimeParameter]] = None, + ) -> None: + """Analyze a given protocol, storing the analysis when complete.""" + assert self._runner is not None + try: + result = await self._runner.run( + deck_configuration=[], + ) except BaseException as error: - internal_error = em.map_unexpected_error(error=error) - await self._analysis_store.update( + await self.update_to_failed_analysis( analysis_id=analysis_id, - robot_type=protocol_resource.source.robot_type, - # TODO (spp, 2024-03-12): populate the RTP field if we decide to have - # parameter parsing and validation in protocol reader itself. - run_time_parameters=[], - commands=[], - labware=[], - modules=[], - pipettes=[], - errors=[ - ErrorOccurrence.from_failed( - # TODO(tz, 2-15-24): replace with a different error type - # when we are able to support different errors. - id="internal-error", - createdAt=datetime_helper.utc_now(), - error=internal_error, - ) - ], - liquids=[], + protocol_robot_type=protocol_resource.source.robot_type, + error=error, + run_time_parameters=run_time_parameters or [], ) return @@ -80,3 +84,32 @@ async def analyze( errors=result.state_summary.errors, liquids=result.state_summary.liquids, ) + + async def update_to_failed_analysis( + self, + analysis_id: str, + protocol_robot_type: RobotType, + error: BaseException, + run_time_parameters: List[RunTimeParameter], + ) -> None: + """Update analysis store with analysis failure.""" + internal_error = em.map_unexpected_error(error=error) + await self._analysis_store.update( + analysis_id=analysis_id, + robot_type=protocol_robot_type, + run_time_parameters=run_time_parameters, + commands=[], + labware=[], + modules=[], + pipettes=[], + errors=[ + ErrorOccurrence.from_failed( + # TODO(tz, 2-15-24): replace with a different error type + # when we are able to support different errors. + id="internal-error", + createdAt=datetime_helper.utc_now(), + error=internal_error, + ) + ], + liquids=[], + ) diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index d3375f535d4..d9314d0be40 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -311,17 +311,35 @@ async def create_protocol( protocol_auto_deleter.make_room_for_new_protocol() protocol_store.insert(protocol_resource) - - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=protocol_resource, - analysis_id=analysis_id, - run_time_param_values=parsed_rtp, - ) - pending_analysis = analysis_store.add_pending( - protocol_id=protocol_id, - analysis_id=analysis_id, - ) + try: + run_time_parameters = await protocol_analyzer.load_runner( + protocol_resource=protocol_resource, + run_time_param_values=parsed_rtp, + ) + except BaseException as error: + analysis_store.add_pending( + protocol_id=protocol_id, + analysis_id=analysis_id, + run_time_parameters=[], + ) + await protocol_analyzer.update_to_failed_analysis( + analysis_id=analysis_id, + protocol_robot_type=protocol_resource.source.robot_type, + error=error, + run_time_parameters=[], + ) + else: + analysis_store.add_pending( + protocol_id=protocol_id, + analysis_id=analysis_id, + run_time_parameters=run_time_parameters, + ) + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=protocol_resource, + analysis_id=analysis_id, + run_time_parameters=run_time_parameters, + ) data = Protocol( id=protocol_id, @@ -329,7 +347,7 @@ async def create_protocol( protocolType=source.config.protocol_type, robotType=source.robot_type, metadata=Metadata.parse_obj(source.metadata), - analysisSummaries=[pending_analysis], + analysisSummaries=analysis_store.get_summaries_by_protocol(protocol_id), key=key, files=[ProtocolFile(name=f.path.name, role=f.role) for f in source.files], ) @@ -373,19 +391,39 @@ async def _start_new_analysis_if_necessary( analysis_summary=analyses[-1], new_rtp_values=rtp_values ) ): - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=resource, - analysis_id=analysis_id, - run_time_param_values=rtp_values, - ) started_new_analysis = True - analyses.append( + try: + run_time_parameters = await protocol_analyzer.load_runner( + protocol_resource=resource, + run_time_param_values=rtp_values, + ) + except BaseException as error: analysis_store.add_pending( protocol_id=protocol_id, analysis_id=analysis_id, + run_time_parameters=[], ) - ) + await protocol_analyzer.update_to_failed_analysis( + analysis_id=analysis_id, + protocol_robot_type=resource.source.robot_type, + error=error, + run_time_parameters=[], + ) + else: + analyses.append( + analysis_store.add_pending( + protocol_id=protocol_id, + analysis_id=analysis_id, + run_time_parameters=run_time_parameters, + ) + ) + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=resource, + analysis_id=analysis_id, + run_time_parameters=run_time_parameters, + ) + return analyses, started_new_analysis diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index c0c63e9e34f..0bf33d44da5 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -163,7 +163,6 @@ async def test_analyze( await subject.analyze( protocol_resource=protocol_resource, analysis_id="analysis-id", - run_time_param_values=None, ) decoy.verify( @@ -246,7 +245,6 @@ async def test_analyze_updates_pending_on_error( await subject.analyze( protocol_resource=protocol_resource, analysis_id="analysis-id", - run_time_param_values=None, ) decoy.verify(