Skip to content
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
27 changes: 16 additions & 11 deletions camacqplugins/gain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@
import voluptuous as vol
from scipy.optimize import curve_fit

from camacq.const import CHANNEL_ID, WELL, WELL_NAME
from camacq.event import Event
from camacq.plugins.sample import Channel
from camacq.helper import BASE_ACTION_SCHEMA
from camacq.image import make_proj
from camacq.util import write_csv

matplotlib.use("AGG") # use noninteractive default backend
# pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports
Expand All @@ -26,6 +23,7 @@
BOX = "box"
COUNT = "count"
VALID = "valid"
CHANNEL_ID = "C{:02d}"
CONF_CHANNEL = "channel"
CONF_CHANNELS = "channels"
CONF_GAIN = "gain"
Expand All @@ -34,6 +32,8 @@
COUNT_CLOSE_TO_ZERO = 2
GAIN_CALC_EVENT = "gain_calc_event"
SAVED_GAINS = "saved_gains"
WELL = "well"
WELL_NAME = "U{:02d}--V{:02d}"

ACTION_CALC_GAIN = "calc_gain"
CALC_GAIN_ACTION_SCHEMA = BASE_ACTION_SCHEMA.extend(
Expand All @@ -60,26 +60,29 @@

GAIN = "gain"
Data = namedtuple("Data", [BOX, GAIN, VALID]) # pylint: disable=invalid-name
Channel = namedtuple("Channel", ["name", GAIN]) # pylint: disable=invalid-name


async def setup_module(center, config):
"""Set up gain calculation plugin."""

async def handle_calc_gain(**kwargs):
"""Handle call to calc_gain action."""
well_x = kwargs.get("well_x")
well_y = kwargs.get("well_y")
plate_name = kwargs.get("plate_name")
well_x = kwargs["well_x"]
well_y = kwargs["well_y"]
plate_name = kwargs["plate_name"]
paths = kwargs.get("images") # list of paths to calculate gain for
if not paths:
well = center.sample.get_well(plate_name, well_x, well_y)
well = center.samples.leica.get_sample(
"well", plate_name=plate_name, well_x=well_x, well_y=well_y
)
if not well:
return
images = {path: image.channel_id for path, image in well.images.items()}
else:
images = {
path: image.channel_id
for path, image in center.sample.images.items()
for path, image in center.samples.leica.images.items()
if path in paths
}
projs = await center.add_executor_job(make_proj, images)
Expand Down Expand Up @@ -108,7 +111,7 @@ async def calc_gain(
]

# This should be a path to a base file name, not to a dir or file.
plot_path = plot_dir / f"U{well_x:02}--V{well_y:02}"
plot_path = plot_dir / WELL_NAME.format(well_x, well_y)
gains = await center.add_executor_job(
partial(_calc_gain, projs, init_gain, plot=make_plots, save_path=plot_path)
)
Expand Down Expand Up @@ -209,7 +212,7 @@ def _calc_gain(projs, init_gain, plot=True, save_path=""):
y_data = roi[BOX].astype(float).values
coeffs, _ = curve_fit(_power_func, x_data, y_data, p0=(1000, -1))
if plot:
_save_path = "{}{}.ome.png".format(save_path, CHANNEL_ID.format(c_id))
_save_path = "{}_{}.ome.png".format(save_path, CHANNEL_ID.format(c_id))
_create_plot(
_save_path, hist_data[COUNT], hist_data[BOX], coeffs, "count-box"
)
Expand Down Expand Up @@ -262,7 +265,9 @@ def _calc_gain(projs, init_gain, plot=True, save_path=""):
def save_gain(save_dir, saved_gains, header):
"""Save a csv file with gain values per image channel."""
path = os.path.normpath(os.path.join(save_dir, "output_gains.csv"))
write_csv(path, saved_gains, header)
data = pd.DataFrame.from_dict(saved_gains, orient="index", columns=[header[1:]])
data.index.name = header[0]
data.to_csv(path)


def ensure_plot_dir(plot_dir):
Expand Down
18 changes: 10 additions & 8 deletions camacqplugins/production/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ production:
```

Each row in the csv file should represent at least one state of a sample container,
ie well, field or channel. A plate name must also be included. The csv file should have a
header. See below.
ie well, field, channel or z_slice. A plate name must also be included. The csv file should have a
header. The first column should have the name of the most low level container to create.
Eg a field must be part of a well which must be part of a plate, so field is the most low level
container of those containers. See below.

```csv
plate_name,well_x,well_y,channel_name,gain
00,1,1,blue,600
name,plate_name,well_x,well_y,channel_id
channel,00,1,1,0
```

This example will set a plate '00', a well (1, 1), a blue channel
and set the gain of the blue channel to 600.
This example will set a plate '00', a well (1, 1), and a channel
with channel id 0.

```csv
plate_name,well_x,well_y,field_x,field_y
00,1,1,1,1
name,plate_name,well_x,well_y,field_x,field_y
field,00,1,1,1,1
```

This example will create a plate '00' a well (1, 1) and a field (1, 1)
Expand Down
85 changes: 55 additions & 30 deletions camacqplugins/production/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
import logging
from math import ceil

import pandas as pd
import voluptuous as vol

from camacq.const import CAMACQ_START_EVENT, CHANNEL_EVENT, IMAGE_EVENT, WELL_EVENT
from camacq.const import CAMACQ_START_EVENT, IMAGE_EVENT
from camacq.event import match_event
from camacq.plugins.leica.command import cam_com, del_com, gain_com
from camacq.plugins.sample import ACTION_TO_METHOD, ACTION_SET_PLATE
from camacq.plugins.sample.helper import next_well_xy
from camacq.util import read_csv
from camacq.plugins.leica.sample import (
CHANNEL_EVENT,
SET_SAMPLE_SCHEMA,
WELL_EVENT,
next_well_xy,
)
from camacq.plugins.sample import get_matched_samples

_LOGGER = logging.getLogger(__name__)

Expand All @@ -35,6 +40,17 @@
SAMPLE_WELL_Y = "well_y"


def read_csv(path):
"""Return a list where each item is a row and dict."""
try:
data = pd.read_csv(path, dtype=str)
data = data.fillna(value="")
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed to read csv file: %s", exc)
raise vol.Invalid from exc
return data.to_dict(orient="records")


@vol.truth
def is_csv(value):
"""Return true if value ends with .csv."""
Expand All @@ -46,28 +62,34 @@ def is_sample_state(value):

At least one sample action must validate per sample data item.
"""
schemas = list(SET_SAMPLE_SCHEMA.validators)
for idx, data in enumerate(value):
valid = False
error = None
for action, settings in ACTION_TO_METHOD.items():
if action == ACTION_SET_PLATE:
sample_name = data.get("name")
for schema in schemas:
if (
schema.schema["name"] in ("plate", "image")
or schema.schema["name"] != sample_name
):
continue
schema = settings["schema"]
try:
data.update(schema(data))
except vol.Invalid as exc:
error = exc
continue
else:
valid = True
valid = True
break

if not valid:
_LOGGER.error(
"The sample state file contains invalid data at row %s: %s",
idx + 2,
error,
)
raise error
if error:
raise error
raise vol.Invalid("Invalid sample state file")

return value

Expand Down Expand Up @@ -155,24 +177,16 @@ async def setup(self, state_data):
async def load_sample(self, state_data):
"""Load sample state."""
for data in state_data:
if data[SAMPLE_PLATE_NAME] not in self._center.sample.plates:
await self._center.actions.sample.set_plate(silent=True, **data)
well_coord = data[SAMPLE_WELL_X], data[SAMPLE_WELL_Y]
self.wells_left.add(well_coord)
if (
well_coord
not in self._center.sample.plates[data[SAMPLE_PLATE_NAME]].wells
):
await self._center.actions.sample.set_well(silent=True, **data)
await self._center.actions.sample.set_channel(silent=True, **data)
await self._center.actions.sample.set_field(silent=True, **data)
await self._center.actions.sample.set_sample(silent=True, **data)

def image_next_well_on_sample(self):
"""Image next well in existing sample."""

async def send_cam_job(center, event):
"""Run on well event."""
next_well_x, next_well_y = next_well_xy(center.sample, PLATE_NAME)
next_well_x, next_well_y = next_well_xy(center.samples.leica, PLATE_NAME)

if (
not match_event(event, event_type=CAMACQ_START_EVENT)
Expand All @@ -187,7 +201,7 @@ async def send_cam_job(center, event):
):
return

if center.sample.images:
if center.samples.leica.images:
await center.actions.command.stop_imaging()
await self.send_gain_jobs(
next_well_x, next_well_y,
Expand Down Expand Up @@ -234,10 +248,10 @@ def set_exp_gain(self):

async def set_gain(center, event):
"""Set pmt gain."""
channel = next(
channel_id, channel = next(
(
channel
for channel in self.channels
(channel_id, channel)
for channel_id, channel in enumerate(self.channels)
if event.channel_name == channel[CONF_CHANNEL]
)
)
Expand All @@ -250,12 +264,13 @@ async def set_gain(center, event):
# Set the gain at the microscope.
await center.actions.command.send(command=command)
# Set the gain in the sample state.
await center.actions.sample.set_channel(
await center.actions.sample.set_sample(
name="channel",
plate_name=event.plate_name,
well_x=event.well_x,
well_y=event.well_y,
channel_name=event.channel_name,
gain=gain,
channel_id=channel_id,
values={"channel_name": event.channel_name, "gain": gain},
)

return self._center.bus.register("gain_calc_event", set_gain)
Expand All @@ -266,8 +281,17 @@ def add_exp_job(self):
async def add_cam_job(center, event):
"""Add an experiment job to the cam list."""
last_channel = self.channels[-1]
channels = get_matched_samples(
center.samples.leica,
"channel",
{
"plate_name": event.plate_name,
"well_x": event.well_x,
"well_y": event.well_y,
},
)
if not match_event(event, channel_name=last_channel[CONF_CHANNEL]) or len(
event.well.channels
channels
) != len(self.channels):
return

Expand Down Expand Up @@ -374,13 +398,14 @@ async def set_sample_img_ok(self, center, event):
if not match_event(event, job_id=self.exp_job_ids[-1]):
return

await center.actions.sample.set_field(
await center.actions.sample.set_sample(
name="field",
plate_name=event.plate_name,
well_x=event.well_x,
well_y=event.well_y,
field_x=event.field_x,
field_y=event.field_y,
img_ok=True,
values={"img_ok": True},
)


Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
camacq==0.5.0
camacq==0.6.0
matplotlib==3.1.2
pandas==0.25.3
scipy==1.3.3
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
VERSION = (PROJECT_DIR / "camacqplugins" / "VERSION").read_text().strip()
README_FILE = PROJECT_DIR / "README.md"
LONG_DESCR = README_FILE.read_text(encoding="utf-8")
REQUIRES = ["camacq>=0.5.0", "matplotlib", "pandas", "scipy"]
REQUIRES = ["camacq>=0.6.0", "matplotlib", "pandas", "scipy"]


setuptools.setup(
Expand Down
11 changes: 9 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
import pytest

from camacq.control import Center
from camacq.plugins.leica import sample as leica_sample_mod


@pytest.fixture
def center(event_loop):
@pytest.fixture(name="center")
def center_fixture(event_loop):
"""Give access to center via fixture."""
_center = Center(loop=event_loop)
_center._track_tasks = True # pylint: disable=protected-access
yield _center


@pytest.fixture(name="leica_sample")
async def leica_sample_fixture(center):
"""Mock leica sample."""
await leica_sample_mod.setup_module(center, {})
2 changes: 1 addition & 1 deletion tests/gain/test_gain.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def load_image_fixture():
yield load_image


async def test_gain(center, load_image):
async def test_gain(center, leica_sample, load_image):
"""Run gain calculation test."""
config = {
"gain": {
Expand Down
Loading