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

API work to enable calibration experience #352

Merged
merged 8 commits into from
Sep 28, 2017
10 changes: 10 additions & 0 deletions api/opentrons/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .session import Session, SessionManager
from .routers import MainRouter
from .calibration import CalibrationManager

__all__ = [
Session,
SessionManager,
MainRouter,
CalibrationManager
]
21 changes: 21 additions & 0 deletions api/opentrons/api/calibration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class CalibrationManager:
def __init__(self, loop=None):
self._loop = loop

def tip_probe(self, instrument):
raise NotImplemented()

def move_to_front(self, instrument):
# instrument.move_to(PIPETTE_CHANGE_POSITION)
raise NotImplemented()

def move_to(self, instrument, obj):
# instrument.move_to(obj[0])
raise NotImplemented()

def jog(self, instrument, coordinates):
# instrument.jog(coordinates)
raise NotImplemented()

def update_container_offset(self):
raise NotImplemented()
35 changes: 35 additions & 0 deletions api/opentrons/api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class Container:
def __init__(self, container, instruments=None):
instruments = instruments or []
self.id = id(container)
self.name = container.get_name()
self.type = container.get_type()
self.slot = container.parent.get_name()
self.instruments = [
Instrument(instrument)
for instrument in instruments]
self.wells = [Well(well) for well in container]


class Well:
def __init__(self, well):
self.id = id(well)
self.properties = well.properties.copy()
self.coordinates = well.coordinates(reference=well.parent)


class Instrument:
def __init__(self, instrument, containers=None):
containers = containers or []
self._instruments = instrument
self.id = id(instrument)
self.name = instrument.name
self.channels = instrument.channels
self.axis = instrument.axis
self.tip_racks = [
Container(container)
for container in instrument.tip_racks]
self.containers = [
Container(container)
for container in containers
]
23 changes: 23 additions & 0 deletions api/opentrons/api/routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from .session import SessionManager, Session
from .calibration import CalibrationManager
from opentrons.broker import subscribe, Notifications


class MainRouter:
def __init__(self, loop=None):
self._notifications = Notifications(loop=loop)
self.session_manager = SessionManager(loop=loop)
self.calibration_manager = CalibrationManager(loop=loop)
self._unsubscribe = subscribe(
Session.TOPIC,
self._notifications.on_notify)

@property
def notifications(self):
return self._notifications

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self._unsubscribe()
178 changes: 116 additions & 62 deletions api/opentrons/session/session.py → api/opentrons/api/session.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,75 @@
import ast
from datetime import datetime
from functools import reduce

from .models import Container, Instrument

from opentrons.commands import tree
from opentrons.broker import publish, subscribe
from opentrons.commands import tree, types
from opentrons import robot
from opentrons.robot.robot import Robot
from datetime import datetime
from opentrons.containers import get_container

from opentrons.broker import publish, subscribe, Notifications
from opentrons.commands import types


VALID_STATES = set(
['loaded', 'running', 'finished', 'stopped', 'paused'])
SESSION_TOPIC = 'session'
VALID_STATES = {'loaded', 'running', 'finished', 'stopped', 'paused'}


class SessionManager(object):
def __init__(self, loop=None):
self._notifications = Notifications(loop=loop)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

self._unsubscribe = subscribe(
SESSION_TOPIC, self._notifications.on_notify)
self.session = None
self.robot = Robot()
# TODO (artyom, 09182017): This is to support the future
# concept of archived sessions. To be reworked when more details
# are available
self.sessions = []

@property
def notifications(self):
return self._notifications

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self.clear()
self._unsubscribe()

def clear(self):
for session in self.sessions:
session.close()
self.sessions.clear()

def create(self, name, text):
self.clear()

with self._notifications.snooze():
self.session = Session(name=name, text=text)
self.sessions.append(self.session)
# Can't do it from session's __init__ because notifications are snoozed
self.session.set_state('loaded')
self.session = Session(name=name, text=text)
return self.session

def get_session(self):
return self.session


class Session(object):
TOPIC = 'session'

def __init__(self, name, text):
self.name = name
self.protocol_text = text
self.protocol = None
self.state = None
self._unsubscribe = subscribe(types.COMMAND, self.on_command)
self.commands = []
self.command_log = {}
self.errors = []

try:
self.refresh()
except Exception as e:
self.close()
raise e

def on_command(self, message):
if message['$'] == 'before':
self.log_append()

def close(self):
self._unsubscribe()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self.close()
self._containers = []
self._instruments = []
self._interactions = []

self.refresh()

def get_instruments(self):
return [
Instrument(
instrument=instrument,
containers=[
container
for _instrument, container in
self._interactions
if _instrument == instrument
])
for instrument in self._instruments
]

def get_containers(self):
return [
Container(
container=container,
instruments=[
instrument
for instrument, _container in
self._interactions
if _container == container
])
for container in self._containers
]

def clear_logs(self):
self.command_log.clear()
Expand All @@ -93,6 +78,11 @@ def clear_logs(self):
def _simulate(self):
stack = []
res = []
commands = []

self._containers.clear()
self._instruments.clear()
self._interactions.clear()

def on_command(message):
payload = message['payload']
Expand All @@ -101,6 +91,8 @@ def on_command(message):
)

if message['$'] == 'before':
commands.append(payload)

res.append(
{
'level': len(stack),
Expand All @@ -117,6 +109,14 @@ def on_command(message):
finally:
unsubscribe()

# Accumulate containers, instruments, interactions from commands
containers, instruments, interactions = _accumulate(
[_get_labware(command) for command in commands])

self._containers.extend(_dedupe(containers))
self._instruments.extend(_dedupe(instruments))
self._interactions.extend(_dedupe(interactions))

return res

def refresh(self):
Expand All @@ -126,7 +126,6 @@ def refresh(self):
parsed = ast.parse(self.protocol_text)
self.protocol = compile(parsed, filename=self.name, mode='exec')
self.commands = tree.from_list(self._simulate())
self.command_log.clear()
finally:
if self.errors:
raise Exception(*self.errors)
Expand All @@ -153,8 +152,14 @@ def run(self, devicename=None):
# with the one from a newly constructed robot
robot.__dict__ = {**Robot().__dict__}
self.clear_logs()
_unsubscribe = None

def on_command(message):
if message['$'] == 'before':
self.log_append()

if devicename is not None:
_unsubscribe = subscribe(types.COMMAND, on_command)
self.set_state('running')
robot.connect(devicename)

Expand All @@ -164,15 +169,21 @@ def run(self, devicename=None):
self.error_append(e)
raise e
finally:
self.set_state('finished')
if _unsubscribe:
_unsubscribe()
# TODO (artyom, 20170927): we should fully separate
# run and simulate code
if devicename is not None:
self.set_state('finished')
robot.disconnect()

return self

def set_state(self, state):
if state not in VALID_STATES:
raise ValueError('Invalid state: {0}. Valid states are: {1}'
.format(state, VALID_STATES))
raise ValueError(
'Invalid state: {0}. Valid states are: {1}'
.format(state, VALID_STATES))
self.state = state
self._on_state_changed()

Expand All @@ -195,6 +206,7 @@ def error_append(self, error):

def _snapshot(self):
return {
'topic': Session.TOPIC,
'name': 'state',
'payload': {
'name': self.name,
Expand All @@ -207,4 +219,46 @@ def _snapshot(self):
}

def _on_state_changed(self):
publish(SESSION_TOPIC, self._snapshot())
publish(Session.TOPIC, self._snapshot())


def _accumulate(iterable):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is doing a union on an iterable of tuples?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will expect an iterable of tuple of lists and accumulate lists in each component of a tuple. Sounds like a union, except for this is not a set.

return reduce(
lambda x, y: tuple([x + y for x, y in zip(x, y)]),
iterable,
([], [], []))


def _dedupe(iterable):
acc = set()

for item in iterable:
if item not in acc:
acc.add(item)
yield item


def _get_labware(command):
containers = []
instruments = []
interactions = []

location = command.get('location')
instrument = command.get('instrument')
locations = command.get('locations')

if location:
containers.append(get_container(location))

if locations:
containers.extend(
[get_container(location) for location in locations])

containers = [c for c in containers if c is not None]

if instrument:
instruments.append(instrument)
interactions.extend(
[(instrument, container) for container in containers])

return instruments, containers, interactions
Loading