Skip to content

Commit fb55f12

Browse files
authored
refactor(robot-server): pull sessions/analysis equipment from engine (#8320)
1 parent 70b956f commit fb55f12

File tree

15 files changed

+343
-226
lines changed

15 files changed

+343
-226
lines changed

api/src/opentrons/protocol_runner/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
The main export of this module is the ProtocolRunner class. See
44
protocol_runner.py for more details.
55
"""
6-
from .protocol_runner import ProtocolRunner
6+
from .protocol_runner import ProtocolRunner, ProtocolRunData
77
from .protocol_file import ProtocolFile, ProtocolFileType
88
from .create_simulating_runner import create_simulating_runner
99

1010
__all__ = [
1111
"ProtocolRunner",
12+
"ProtocolRunData",
1213
"ProtocolFile",
1314
"ProtocolFileType",
1415
"create_simulating_runner",

api/src/opentrons/protocol_runner/protocol_runner.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
"""Protocol run control and management."""
2-
from typing import Optional, Sequence
2+
from dataclasses import dataclass
3+
from typing import List, Optional
34

4-
from opentrons.protocol_engine import ProtocolEngine, Command as ProtocolCommand
5+
from opentrons.protocol_engine import (
6+
ProtocolEngine,
7+
Command,
8+
LoadedLabware,
9+
LoadedPipette,
10+
)
511

612
from .protocol_file import ProtocolFile, ProtocolFileType
713
from .task_queue import TaskQueue, TaskQueuePhase
@@ -12,6 +18,15 @@
1218
from .python_executor import PythonExecutor
1319

1420

21+
@dataclass(frozen=True)
22+
class ProtocolRunData:
23+
"""Data from a protocol run."""
24+
25+
commands: List[Command]
26+
labware: List[LoadedLabware]
27+
pipettes: List[LoadedPipette]
28+
29+
1530
class ProtocolRunner:
1631
"""An interface to manage and control a protocol run.
1732
@@ -45,11 +60,6 @@ def __init__(
4560
self._python_context_creator = python_context_creator or PythonContextCreator()
4661
self._python_executor = python_executor or PythonExecutor()
4762

48-
@property
49-
def engine(self) -> ProtocolEngine:
50-
"""Get the runner's underlying ProtocolEngine."""
51-
return self._protocol_engine
52-
5363
def load(self, protocol_file: ProtocolFile) -> None:
5464
"""Load a ProtocolFile into managed ProtocolEngine.
5565
@@ -92,12 +102,17 @@ async def join(self) -> None:
92102
"""
93103
return await self._task_queue.join()
94104

95-
async def run(self, protocol_file: ProtocolFile) -> Sequence[ProtocolCommand]:
105+
async def run(self, protocol_file: ProtocolFile) -> ProtocolRunData:
96106
"""Run a given protocol to completion."""
97107
self.load(protocol_file)
98108
self.play()
99109
await self.join()
100-
return self._protocol_engine.state_view.commands.get_all()
110+
111+
commands = self._protocol_engine.state_view.commands.get_all()
112+
labware = self._protocol_engine.state_view.labware.get_all()
113+
pipettes = self._protocol_engine.state_view.pipettes.get_all()
114+
115+
return ProtocolRunData(commands=commands, labware=labware, pipettes=pipettes)
101116

102117
def _load_json(self, protocol_file: ProtocolFile) -> None:
103118
protocol = self._json_file_reader.read(protocol_file)

api/src/opentrons/protocol_runner/python_executor.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,16 @@
1010
class PythonExecutor:
1111
"""Execute a given PythonProtocol's run method with a ProtocolContext."""
1212

13-
def __init__(self) -> None:
14-
"""Initialize the executor with a thread pool.
15-
16-
A PythonExecutor uses its own ThreadPoolExecutor (rather than the default)
17-
to avoid thread pool exhaustion from tying up protocol execution.
18-
"""
19-
self._loop = asyncio.get_running_loop()
20-
# fixme(mm, 2021-08-09): This class should be a context manager and call
21-
# self._thread_pool.shutdown(). Currently, I think we leak threads.
22-
self._thread_pool = ThreadPoolExecutor(max_workers=1)
23-
24-
async def execute(self, protocol: PythonProtocol, context: ProtocolContext) -> None:
13+
@staticmethod
14+
async def execute(protocol: PythonProtocol, context: ProtocolContext) -> None:
2515
"""Execute a PythonProtocol using the given ProtocolContext.
2616
2717
Runs the protocol asynchronously in a child thread.
2818
"""
29-
await self._loop.run_in_executor(
30-
executor=self._thread_pool,
31-
func=partial(protocol.run, context),
32-
)
19+
loop = asyncio.get_running_loop()
20+
21+
with ThreadPoolExecutor(max_workers=1) as executor:
22+
await loop.run_in_executor(
23+
executor=executor,
24+
func=partial(protocol.run, context),
25+
)

api/tests/opentrons/protocol_runner/test_protocol_runner_smoke.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
from opentrons.protocol_api_experimental import DeckSlotName
1717

1818
from opentrons.protocol_engine import (
19+
DeckSlotLocation,
1920
LoadedLabware,
2021
LoadedPipette,
2122
PipetteName,
22-
DeckSlotLocation,
2323
commands,
2424
)
2525
from opentrons.protocol_runner import (
@@ -37,22 +37,25 @@ async def test_protocol_runner_with_python(python_protocol_file: Path) -> None:
3737
)
3838

3939
subject = await create_simulating_runner()
40-
commands_result = await subject.run(protocol_file)
41-
pipettes_result = subject.engine.state_view.pipettes.get_all()
42-
labware_result = subject.engine.state_view.labware.get_all()
40+
result = await subject.run(protocol_file)
41+
commands_result = result.commands
42+
pipettes_result = result.pipettes
43+
labware_result = result.labware
4344

4445
pipette_id_captor = matchers.Captor()
4546
labware_id_captor = matchers.Captor()
4647

4748
expected_pipette = LoadedPipette.construct(
48-
id=pipette_id_captor, pipetteName=PipetteName.P300_SINGLE, mount=MountType.LEFT
49+
id=pipette_id_captor,
50+
pipetteName=PipetteName.P300_SINGLE,
51+
mount=MountType.LEFT,
4952
)
5053

5154
expected_labware = LoadedLabware.construct(
5255
id=labware_id_captor,
56+
location=DeckSlotLocation(slot=DeckSlotName.SLOT_1),
5357
loadName="opentrons_96_tiprack_300ul",
5458
definitionUri="opentrons/opentrons_96_tiprack_300ul/1",
55-
location=DeckSlotLocation(slot=DeckSlotName.SLOT_1),
5659
)
5760

5861
assert expected_pipette in pipettes_result
@@ -83,9 +86,10 @@ async def test_protocol_runner_with_json(json_protocol_file: Path) -> None:
8386
)
8487

8588
subject = await create_simulating_runner()
86-
commands_result = await subject.run(protocol_file)
87-
pipettes_result = subject.engine.state_view.pipettes.get_all()
88-
labware_result = subject.engine.state_view.labware.get_all()
89+
result = await subject.run(protocol_file)
90+
commands_result = result.commands
91+
pipettes_result = result.pipettes
92+
labware_result = result.labware
8993

9094
expected_pipette = LoadedPipette(
9195
id="pipette-id",
@@ -95,9 +99,9 @@ async def test_protocol_runner_with_json(json_protocol_file: Path) -> None:
9599

96100
expected_labware = LoadedLabware(
97101
id="labware-id",
102+
location=DeckSlotLocation(slot=DeckSlotName.SLOT_1),
98103
loadName="opentrons_96_tiprack_300ul",
99104
definitionUri="opentrons/opentrons_96_tiprack_300ul/1",
100-
location=DeckSlotLocation(slot=DeckSlotName.SLOT_1),
101105
)
102106

103107
assert expected_pipette in pipettes_result

robot-server/robot_server/protocols/analysis_models.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@
55
from typing import List, Union
66
from typing_extensions import Literal
77

8-
from opentrons.types import MountType
9-
from opentrons.protocol_engine import (
10-
Command as EngineCommand,
11-
PipetteName,
12-
LabwareLocation,
13-
)
8+
from opentrons.protocol_engine import Command, LoadedLabware, LoadedPipette
149

1510

1611
class AnalysisStatus(str, Enum):
@@ -52,23 +47,6 @@ class PendingAnalysis(AnalysisSummary):
5247
status: Literal[AnalysisStatus.PENDING] = AnalysisStatus.PENDING
5348

5449

55-
class AnalysisPipette(BaseModel):
56-
"""A pipette that the protocol is expected to use, based on the analysis."""
57-
58-
id: str
59-
pipetteName: PipetteName
60-
mount: MountType
61-
62-
63-
class AnalysisLabware(BaseModel):
64-
"""A labware that the protocol is expected to use, based on the analysis."""
65-
66-
id: str
67-
loadName: str
68-
definitionUri: str
69-
location: LabwareLocation
70-
71-
7250
class CompletedAnalysis(AnalysisSummary):
7351
"""A completed protocol run analysis.
7452
@@ -95,15 +73,15 @@ class CompletedAnalysis(AnalysisSummary):
9573
...,
9674
description="Whether the protocol is expected to run successfully",
9775
)
98-
pipettes: List[AnalysisPipette] = Field(
76+
pipettes: List[LoadedPipette] = Field(
9977
...,
10078
description="Pipettes used by the protocol",
10179
)
102-
labware: List[AnalysisLabware] = Field(
80+
labware: List[LoadedLabware] = Field(
10381
...,
10482
description="Labware used by the protocol",
10583
)
106-
commands: List[EngineCommand] = Field(
84+
commands: List[Command] = Field(
10785
...,
10886
description="The protocol commands the run is expected to produce",
10987
)

robot-server/robot_server/protocols/analysis_store.py

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
"""Protocol analysis storage."""
22
from typing import Dict, List, Set, Sequence
33

4-
from opentrons.calibration_storage.helpers import uri_from_details
5-
from opentrons.protocol_engine import commands as pe_commands
4+
from opentrons.protocol_engine import (
5+
Command,
6+
CommandStatus,
7+
LoadedPipette,
8+
LoadedLabware,
9+
)
610

711
from .analysis_models import (
812
ProtocolAnalysis,
913
PendingAnalysis,
1014
CompletedAnalysis,
1115
AnalysisResult,
12-
AnalysisLabware,
13-
AnalysisPipette,
1416
)
1517

1618

@@ -35,41 +37,18 @@ def add_pending(self, protocol_id: str, analysis_id: str) -> List[ProtocolAnalys
3537
def update(
3638
self,
3739
analysis_id: str,
38-
commands: Sequence[pe_commands.Command],
40+
commands: Sequence[Command],
41+
labware: Sequence[LoadedLabware],
42+
pipettes: Sequence[LoadedPipette],
3943
errors: Sequence[Exception],
4044
) -> None:
4145
"""Update analysis results in the store."""
42-
labware = []
43-
pipettes = []
4446
# TODO(mc, 2021-08-25): return error details objects, not strings
4547
error_messages = [str(e) for e in errors]
4648

47-
for c in commands:
48-
if isinstance(c, pe_commands.LoadLabware) and c.result is not None:
49-
labware.append(
50-
AnalysisLabware(
51-
id=c.result.labwareId,
52-
loadName=c.data.loadName,
53-
definitionUri=uri_from_details(
54-
load_name=c.data.loadName,
55-
namespace=c.data.namespace,
56-
version=c.data.version,
57-
),
58-
location=c.data.location,
59-
)
60-
)
61-
elif isinstance(c, pe_commands.LoadPipette) and c.result is not None:
62-
pipettes.append(
63-
AnalysisPipette(
64-
id=c.result.pipetteId,
65-
pipetteName=c.data.pipetteName,
66-
mount=c.data.mount,
67-
)
68-
)
69-
7049
if len(error_messages) > 0:
7150
result = AnalysisResult.ERROR
72-
elif any(c.status == pe_commands.CommandStatus.FAILED for c in commands):
51+
elif any(c.status == CommandStatus.FAILED for c in commands):
7352
result = AnalysisResult.NOT_OK
7453
else:
7554
result = AnalysisResult.OK
@@ -78,9 +57,9 @@ def update(
7857
id=analysis_id,
7958
result=result,
8059
commands=list(commands),
60+
labware=list(labware),
61+
pipettes=list(pipettes),
8162
errors=error_messages,
82-
labware=labware,
83-
pipettes=pipettes,
8463
)
8564

8665
def get_by_protocol(self, protocol_id: str) -> List[ProtocolAnalysis]:

robot-server/robot_server/protocols/protocol_analyzer.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Protocol analysis module."""
2-
from typing import Sequence
3-
from opentrons.protocol_engine import Command as ProtocolCommand
2+
from typing import List
3+
from opentrons.protocol_engine import Command, LoadedLabware, LoadedPipette
44
from opentrons.protocol_runner import ProtocolRunner
55

66
from .protocol_store import ProtocolResource
@@ -25,16 +25,23 @@ async def analyze(
2525
analysis_id: str,
2626
) -> None:
2727
"""Analyze a given protocol, storing the analysis when complete."""
28-
commands: Sequence[ProtocolCommand] = []
29-
errors: Sequence[Exception] = []
28+
commands: List[Command] = []
29+
labware: List[LoadedLabware] = []
30+
pipettes: List[LoadedPipette] = []
31+
errors: List[Exception] = []
3032

3133
try:
32-
commands = await self._protocol_runner.run(protocol_resource)
34+
result = await self._protocol_runner.run(protocol_resource)
35+
commands = result.commands
36+
labware = result.labware
37+
pipettes = result.pipettes
3338
except Exception as e:
3439
errors = [e]
3540

3641
self._analysis_store.update(
3742
analysis_id=analysis_id,
3843
commands=commands,
44+
labware=labware,
45+
pipettes=pipettes,
3946
errors=errors,
4047
)

0 commit comments

Comments
 (0)