Skip to content

Commit

Permalink
Merge pull request #2 from choderalab/plate_reader
Browse files Browse the repository at this point in the history
Plate_reader
  • Loading branch information
mark-polk authored Jan 6, 2025
2 parents fc5f666 + 988316a commit bc60201
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 1 deletion.
81 changes: 80 additions & 1 deletion fluorescence_assay/plate_reader.py
Original file line number Diff line number Diff line change
@@ -1 +1,80 @@
"""Comment."""
"""Module to parse plate reader outputs."""

import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional

from bs4 import BeautifulSoup

logger = logging.getLogger(__name__)


@dataclass
class Measurements(ABC):
""""""

@abstractmethod
def read_file(self, filepath: str, *args, **kwargs) -> None:
""""""
...

@abstractmethod
def get_well(self, *args, **kwargs) -> dict:
""""""
...

@abstractmethod
def get_parameter(self, *args, **kwargs):
""""""
...


@dataclass
class IControlXML(Measurements):
""""""

_data: dict = field(default_factory=dict, init=False)

def read_file(self, filepath: str, filter: Optional[list[str]] = None) -> None:
""""""

with open(filepath) as file:
self._data = BeautifulSoup(file, "xml")

def get_data(self):
""""""

return self._data

def get_well(self, section, well, cycle: Optional[int] = 1):
""""""

def fix_type(val):
try:
return float(val)
except:
return float("nan")

data = {
int(scan["WL"]): fix_type(scan.contents[0])
for scan in self._data.select(
f'Section[Name="{section}"] > Data[Cycle="{str(cycle)}"] > Well[Pos="{well}"] > Scan'
)
}

return data

def get_parameter(self, section, parameter):

def fix_type(val):
try:
return float(val)
except:
return val

return fix_type(
self._data.select(
f'Section[Name="{section}"] > Parameters > Parameter[Name="{parameter}"]'
)[0]["Value"]
)
191 changes: 191 additions & 0 deletions fluorescence_assay/plotting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""Module to plot parsed plate reader ouptputs."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional

import matplotlib
import matplotlib.axes
import matplotlib.pyplot as plt
import numpy as np

from .plate_reader import IControlXML, Measurements


@dataclass
class Plot(ABC):
""""""

@abstractmethod
def load_data(self, Measurement: Measurements, *args, **kwargs) -> None:
""""""
...

def format_plot(
self,
axes: matplotlib.axes._axes.Axes,
title: Optional[str] = None,
xlim: Optional[list[float]] = None,
ylim: Optional[list[float]] = None,
xlabel: Optional[str] = None,
ylabel: Optional[str] = None,
square: Optional[bool] = False,
):

if title is not None:
axes.set_title(title)

if xlim is not None:
axes.set_xlim(xlim)

if ylim is not None:
axes.set_ylim(ylim)

if xlabel is not None:
axes.set_xlabel(xlabel)

if ylabel is not None:
axes.set_ylabel(ylabel)

if square == True:
axes.set_box_aspect(1)


@dataclass
class IControlXMLPlot(Plot):
""""""

_plate_read: dict = field(default_factory=dict, init=False)

def load_data(self, IControlXML: IControlXML) -> None:

self._plate_read = IControlXML

def get_wavelength_axis(self, section) -> np.ndarray:

lmin, lmax, lstep = (
self._plate_read.get_parameter(section, parameter)
for parameter in [
"Emission Wavelength Start",
"Emission Wavelength End",
"Emission Wavelength Step Size",
]
)

return np.arange(lmin, lmax + lstep, lstep)

def plot_well_spectrum(
self,
axes: matplotlib.axes._axes.Axes,
section: str,
well: str,
cycle: Optional[int] = 1,
color: Optional[tuple] = (0, 0, 0),
label: Optional[str] = None,
) -> None:

data = np.array(list(self._plate_read.get_well(section, well, cycle).values()))

ll = self.get_wavelength_axis(section)

axes.plot(ll, data, color=color, label=label)

def plot_corrected_spectrum(
self,
axes: matplotlib.axes._axes.Axes,
section: str,
well_foreground: str,
well_background: str,
cycle: Optional[int] = 1,
color: Optional[tuple] = (0, 0, 0),
label: Optional[str] = None,
) -> None:

foreground = np.array(
list(self._plate_read.get_well(section, well_foreground, cycle).values())
)
background = np.array(
list(self._plate_read.get_well(section, well_background, cycle).values())
)

difference = foreground - background

ll = self.get_wavelength_axis(section)

axes.plot(ll, difference, color=color, label=label)

def plot_dose_response(
self,
axes: matplotlib.axes._axes.Axes,
section: str,
row_foreground: str,
row_background: str,
wavelength: int,
concentrations: list[float],
cycle: Optional[int] = 1,
color: Optional[tuple] = (0, 0, 0),
label: Optional[str] = None,
) -> None:

differences = []

for i in range(len(concentrations)):

foreground = self._plate_read.get_well(
section, f"{row_foreground}{i+1}", cycle
)[wavelength]
background = self._plate_read.get_well(
section, f"{row_background}{i+1}", cycle
)[wavelength]

difference = foreground - background

differences.append(difference)

axes.plot(concentrations, differences, ".", color=color, label=label)

def plot_absorption_spectrum(
self,
axes: matplotlib.axes._axes.Axes,
section: str,
well: str,
cycle: Optional[int] = 1,
color: Optional[tuple] = (0, 0, 0),
label: Optional[str] = None,
) -> None:

data = np.array(list(self._plate_read.get_well(section, well, cycle).values()))

lmin, lmax, lstep = (
# hardcoding these values temporarily
# TODO: Undo this!
240,
800,
5,
)

ll = np.arange(lmin, lmax + lstep, lstep)

axes.plot(ll, data, color=color, label=label)

def plot_absorption_across_row(
self,
axes: matplotlib.axes._axes.Axes,
section: str,
row: str,
wavelength: int,
concentrations: list[float],
cycle: Optional[int] = 1,
color: Optional[tuple] = (0, 0, 0),
label: Optional[str] = None,
) -> None:

values = []

for i in range(len(concentrations)):

value = self._plate_read.get_well(section, f"{row}{i+1}", cycle)[wavelength]

values.append(value)

axes.plot(concentrations, values, ".", color=color, label=label)

0 comments on commit bc60201

Please sign in to comment.