Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api, robot-server): set sent runtime values and report parameters in analysis #14735

Merged
merged 14 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions api/src/opentrons/cli/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ async def _analyze(
),
metadata=protocol_source.metadata,
robotType=protocol_source.robot_type,
# TODO(spp, 2024-03-12): update this once protocol reader/ runner can parse
# and report the runTimeParameters
runTimeParameters=[],
runTimeParameters=analysis.parameters,
commands=analysis.commands,
errors=analysis.state_summary.errors,
labware=analysis.state_summary.labware,
Expand Down
125 changes: 78 additions & 47 deletions api/src/opentrons/protocol_api/_parameter_context.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Parameter context for python protocols."""

from typing import List, Optional, Union
from typing import List, Optional, Union, Dict

from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocols.parameters import parameter_definition
from opentrons.protocols.parameters.types import ParameterChoice
from opentrons.protocols.parameters import parameter_definition, validation
from opentrons.protocols.parameters.types import (
ParameterChoice,
ParameterDefinitionError,
)
from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType

from ._parameters import Parameters

Expand All @@ -22,7 +26,7 @@ class ParameterContext:
def __init__(self, api_version: APIVersion) -> None:
"""Initializes a parameter context for user-set parameters."""
self._api_version = api_version
self._parameters: List[_ParameterDefinitionTypes] = []
self._parameters: Dict[str, _ParameterDefinitionTypes] = {}

def add_int(
self,
Expand All @@ -48,18 +52,17 @@ def add_int(
description: A description of the parameter as it will show up on the frontend.
unit: An optional unit to be appended to the end of the integer as it shown on the frontend.
"""
self._parameters.append(
parameter_definition.create_int_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
minimum=minimum,
maximum=maximum,
choices=choices,
description=description,
unit=unit,
)
parameter = parameter_definition.create_int_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
minimum=minimum,
maximum=maximum,
choices=choices,
description=description,
unit=unit,
)
self._parameters[parameter.variable_name] = parameter

def add_float(
self,
Expand All @@ -85,18 +88,17 @@ def add_float(
description: A description of the parameter as it will show up on the frontend.
unit: An optional unit to be appended to the end of the float as it shown on the frontend.
"""
self._parameters.append(
parameter_definition.create_float_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
minimum=minimum,
maximum=maximum,
choices=choices,
description=description,
unit=unit,
)
parameter = parameter_definition.create_float_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
minimum=minimum,
maximum=maximum,
choices=choices,
description=description,
unit=unit,
)
self._parameters[parameter.variable_name] = parameter

def add_bool(
self,
Expand All @@ -113,18 +115,17 @@ def add_bool(
default: The default value the boolean parameter will be set to. This will be used in initial analysis.
description: A description of the parameter as it will show up on the frontend.
"""
self._parameters.append(
parameter_definition.create_bool_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
choices=[
{"display_name": "On", "value": True},
{"display_name": "Off", "value": False},
],
description=description,
)
parameter = parameter_definition.create_bool_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
choices=[
{"display_name": "On", "value": True},
{"display_name": "Off", "value": False},
],
description=description,
)
self._parameters[parameter.variable_name] = parameter

def add_str(
self,
Expand All @@ -144,17 +145,47 @@ def add_str(
Mutually exclusive with minimum and maximum.
description: A description of the parameter as it will show up on the frontend.
"""
self._parameters.append(
parameter_definition.create_str_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
choices=choices,
description=description,
)
parameter = parameter_definition.create_str_parameter(
display_name=display_name,
variable_name=variable_name,
default=default,
choices=choices,
description=description,
)
self._parameters[parameter.variable_name] = parameter

def set_parameters(self, parameter_overrides: RunTimeParamValuesType) -> None:
"""Sets parameters to values given by client, validating them as well.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
for variable_name, override_value in parameter_overrides.items():
try:
parameter = self._parameters[variable_name]
except KeyError:
raise ParameterDefinitionError(
f"Parameter {variable_name} is not defined as a parameter for this protocol."
)
validated_value = validation.ensure_value_type(
override_value, parameter.parameter_type
)
parameter.value = validated_value

def export_parameters_for_analysis(self) -> List[RunTimeParameter]:
"""Exports all parameters into a protocol engine models for reporting in analysis.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return [
parameter.as_protocol_engine_type()
for parameter in self._parameters.values()
]

def export_parameters(self) -> Parameters:
def export_parameters_for_protocol(self) -> Parameters:
"""Exports all parameters into a protocol run usable parameters object.

:meta private:
Expand All @@ -164,6 +195,6 @@ def export_parameters(self) -> Parameters:
return Parameters(
parameters={
parameter.variable_name: parameter.value
for parameter in self._parameters
for parameter in self._parameters.values()
}
)
6 changes: 5 additions & 1 deletion api/src/opentrons/protocol_runner/legacy_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ModuleContext as LegacyModuleContext,
Labware as LegacyLabware,
Well as LegacyWell,
ParameterContext,
create_protocol_context,
)
from opentrons.protocol_api.core.engine import ENGINE_CORE_API_VERSION
Expand Down Expand Up @@ -172,10 +173,13 @@ class LegacyExecutor:
async def execute(
protocol: LegacyProtocol,
context: LegacyProtocolContext,
parameter_context: Optional[ParameterContext],
run_time_param_values: Optional[RunTimeParamValuesType],
) -> None:
"""Execute a PAPIv2 protocol with a given ProtocolContext in a child thread."""
await to_thread.run_sync(run_protocol, protocol, context, run_time_param_values)
await to_thread.run_sync(
run_protocol, protocol, context, parameter_context, run_time_param_values
)


__all__ = [
Expand Down
29 changes: 25 additions & 4 deletions api/src/opentrons/protocol_runner/protocol_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from opentrons.hardware_control import HardwareControlAPI
from opentrons import protocol_reader
from opentrons.legacy_broker import LegacyBroker
from opentrons.protocol_api import ParameterContext
from opentrons.protocol_reader import (
ProtocolSource,
JsonProtocolConfig,
Expand Down Expand Up @@ -38,6 +39,7 @@
from ..protocol_engine.types import (
PostRunHardwareState,
DeckConfigurationType,
RunTimeParameter,
RunTimeParamValuesType,
)

Expand All @@ -47,6 +49,7 @@ class RunResult(NamedTuple):

commands: List[Command]
state_summary: StateSummary
parameters: List[RunTimeParameter]


class AbstractRunner(ABC):
Expand Down Expand Up @@ -79,6 +82,11 @@ def broker(self) -> LegacyBroker:
"""
return self._broker

@property
def run_time_parameters(self) -> List[RunTimeParameter]:
"""Parameter definitions defined by protocol, if any. Currently only for python protocols."""
return []

def was_started(self) -> bool:
"""Whether the run has been started.

Expand Down Expand Up @@ -150,6 +158,14 @@ def __init__(
drop_tips_after_run=drop_tips_after_run,
post_run_hardware_state=post_run_hardware_state,
)
self._parameter_context: Optional[ParameterContext] = None

@property
def run_time_parameters(self) -> List[RunTimeParameter]:
"""Parameter definitions defined by protocol, if any. Will always be empty before execution."""
if self._parameter_context is not None:
return self._parameter_context.export_parameters_for_analysis()
return []

async def load(
self,
Expand All @@ -171,6 +187,7 @@ async def load(
protocol = self._legacy_file_reader.read(
protocol_source, labware_definitions, python_parse_mode
)
self._parameter_context = ParameterContext(api_version=protocol.api_level)
equipment_broker = None

if protocol.api_level < LEGACY_PYTHON_API_VERSION_CUTOFF:
Expand All @@ -190,7 +207,7 @@ async def load(
equipment_broker=equipment_broker,
)
initial_home_command = pe_commands.HomeCreate(
# this command homes all axes, including pipette plugner and gripper jaw
# this command homes all axes, including pipette plunger and gripper jaw
params=pe_commands.HomeParams(axes=None)
)

Expand All @@ -201,6 +218,7 @@ async def run_func() -> None:
await self._legacy_executor.execute(
protocol=protocol,
context=context,
parameter_context=self._parameter_context,
run_time_param_values=run_time_param_values,
)

Expand Down Expand Up @@ -228,7 +246,10 @@ async def run( # noqa: D102

run_data = self._protocol_engine.state_view.get_summary()
commands = self._protocol_engine.state_view.commands.get_all()
return RunResult(commands=commands, state_summary=run_data)
parameters = self.run_time_parameters
return RunResult(
commands=commands, state_summary=run_data, parameters=parameters
)


class JsonRunner(AbstractRunner):
Expand Down Expand Up @@ -332,7 +353,7 @@ async def run( # noqa: D102

run_data = self._protocol_engine.state_view.get_summary()
commands = self._protocol_engine.state_view.commands.get_all()
return RunResult(commands=commands, state_summary=run_data)
return RunResult(commands=commands, state_summary=run_data, parameters=[])


class LiveRunner(AbstractRunner):
Expand Down Expand Up @@ -373,7 +394,7 @@ async def run( # noqa: D102

run_data = self._protocol_engine.state_view.get_summary()
commands = self._protocol_engine.state_view.commands.get_all()
return RunResult(commands=commands, state_summary=run_data)
return RunResult(commands=commands, state_summary=run_data, parameters=[])


AnyRunner = Union[PythonAndLegacyRunner, JsonRunner, LiveRunner]
Expand Down
24 changes: 18 additions & 6 deletions api/src/opentrons/protocols/execution/execute.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
from typing import Optional, Dict, Union
from typing import Optional

from opentrons.protocol_api import ProtocolContext
from opentrons.protocol_api import ProtocolContext, ParameterContext
from opentrons.protocol_engine.types import RunTimeParamValuesType
from opentrons.protocols.execution.execute_python import run_python
from opentrons.protocols.execution.json_dispatchers import (
pipette_command_map,
Expand All @@ -20,17 +21,28 @@
def run_protocol(
protocol: Protocol,
context: ProtocolContext,
# TODO (spp, 2024-03-20): move RunTimeParamValuesType to a top level types and use here
run_time_param_overrides: Optional[Dict[str, Union[float, bool, str]]] = None,
parameter_context: Optional[ParameterContext] = None,
run_time_param_overrides: Optional[RunTimeParamValuesType] = None,
) -> None:
"""Run a protocol.

:param protocol: The :py:class:`.protocols.types.Protocol` to execute
:param context: The context to use.
:param context: The protocol context to use.
:param parameter_context: The parameter context to use.
:param run_time_param_overrides: Any parameter values that are potentially overriding the defaults
"""
if isinstance(protocol, PythonProtocol):
if protocol.api_level >= APIVersion(2, 0):
run_python(protocol, context)
# If this is None here then we're either running simulate or execute, in any case we don't need to report
# this in analysis which is the reason we'd pass it to this function
if parameter_context is None:
parameter_context = ParameterContext(protocol.api_level)
run_python(
proto=protocol,
context=context,
parameter_context=parameter_context,
run_time_param_overrides=run_time_param_overrides,
)
else:
raise RuntimeError(f"Unsupported python API version: {protocol.api_level}")
else:
Expand Down
Loading
Loading