Skip to content

Commit

Permalink
Merge branch 'main' into manual_registration
Browse files Browse the repository at this point in the history
  • Loading branch information
talonchandler committed Aug 5, 2023
2 parents fb81b2e + 8457ff7 commit 1a1eb2e
Show file tree
Hide file tree
Showing 24 changed files with 680 additions and 258 deletions.
64 changes: 55 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,77 @@ pip install .

## Usage

Data are acquired using the `mantis run-acquisition` command. A list of the command line arguments can be obtained with:
Mantis acquisitions and analyses use a command-line interface.

A list of `mantis` commands can be displayed with:
```sh
mantis --help
```

Data are acquired using `mantis run-acquisition`, and a list of arguments can be displayed with:

```sh
mantis run-acquisition --help
```

The mantis acquisition is configures using a YAML settings file. An example of a settings file can be found [here](mantis/acquisition/settings/example_acquisition_settings.yaml).
The mantis acquisition is configured using a YAML settings file. An example of a settings file can be found [here](mantis/acquisition/settings/example_acquisition_settings.yaml).

This is an example of a command which will start an acquisition on the mantis microscope:

```pwsh
mantis run-acquisition `
--data-dirpath ./test `
--name test_acquisition `
--settings path/to/settings/file
--config-filepath path/to/config.yaml `
--output-dirpath ./YYYY_MM_DD_experiment_name/acquisition_name
```

The acquisition may also be run in "demo" mode with the Micro-manager `MMConfig_Demo.cfg` config. This does not require any microscope hardware. A demo run can be started with:

```pwsh
mantis run-acquisition `
--data-dirpath ./test `
--name test_acquisition `
--mm-config-file path/to/MMConfig_Demo.cfg `
--settings path/to/settings/file
--config-filepath path/to/config.yaml `
--output-dirpath ./YYYY_MM_DD_experiment_name/acquisition_name `
--mm-config-filepath path/to/MMConfig_Demo.cfg
```

After data has been acquired, we can run analyses from the command line. All analysis calls take an input `-i` and an output `-o`, and the main analysis calls (`deskew`, `reconstruct`, `register`) use configuration files passed via a `-c` flag.

A typical set of CLI calls to go from raw data to registered volumes looks like:

```sh
# CONVERT TO ZARR
iohub convert `
-i ./acq_name/acq_name_labelfree_1 `
-o ./acq_name_labelfree.zarr `
iohub convert `
-i ./acq_name/acq_name_lightsheet_1
-o ./acq_name_lightsheet.zarr

# DESKEW
mantis estimate-deskew `
-i ./acq_name_lightsheet.zarr/0/0/0 `
-o ./deskew.yml
mantis deskew `
-i ./acq_name_lightsheet.zarr/*/*/*
-c ./deskew_params.yml `
-o ./acq_name_lightsheet_deskewed.zarr

# UPCOMING CALLS AHEAD
# RECONSTRUCT
recorder reconstruct `
-i ./acq_name_labelfree.zarr/*/*/* `
-c ./recon.yml `
-o ./acq_name_labelfree_reconstructed.zarr
# REGISTER
mantis estimate-registration `
-ls ./acq_name_lightsheet_deskewed.zarr/0/0/0 `
-lf ./acq_name_labelfree_reconstructed.zarr/0/0/0 `
-o register.yml
mantis register `
-ls ./acq_name_lightsheet_deskewed.zarr `
-lf ./acq_name_lightsheet_deskewed.zarr `
-o ./acq_name_registerred.zarr
```

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion mantis/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.2"
__version__ = "0.1.0"
32 changes: 21 additions & 11 deletions mantis/acquisition/AcquisitionSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@

import numpy as np

from pydantic import ConfigDict, NonNegativeFloat, NonNegativeInt
from pydantic.dataclasses import dataclass

config = ConfigDict(extra='forbid')

@dataclass

@dataclass(config=config)
class ConfigSettings:
config_group: str
config_name: str


@dataclass
@dataclass(config=config)
class DevicePropertySettings:
device_name: str
property_name: str
property_value: str


@dataclass
@dataclass(config=config)
class TimeSettings:
num_timepoints: Optional[int] = 0
time_interval_s: Optional[float] = 0 # in seconds
num_timepoints: NonNegativeInt = 0
time_interval_s: NonNegativeFloat = 0 # in seconds


@dataclass
@dataclass(config=config)
class PositionSettings:
xyz_positions: list = field(default_factory=list)
position_labels: List[str] = field(default_factory=list)
Expand All @@ -36,23 +39,23 @@ def __post_init__(self):
self.num_positions = len(self.xyz_positions)


@dataclass
@dataclass(config=config)
class ChannelSettings:
exposure_time_ms: List[float] = field(default_factory=list) # in ms
exposure_time_ms: List[NonNegativeFloat] = field(default_factory=list) # in ms
channel_group: Optional[str] = None
channels: List[str] = field(default_factory=list)
use_sequencing: bool = False
num_channels: int = field(init=False, default=0)
acquisition_rate: float = field(init=False, default=None)

def __post_init__(self):
self.num_channels = len(self.channels)
assert len(self.exposure_time_ms) == len(
self.channels
), 'Number of channels must equal number of exposure times'
self.num_channels = len(self.channels)


@dataclass
@dataclass(config=config)
class SliceSettings:
z_stage_name: Optional[str] = None
z_start: Optional[float] = None
Expand All @@ -64,12 +67,19 @@ class SliceSettings:
z_range: List[float] = field(init=False, repr=False, default_factory=list)

def __post_init__(self):
# If one of z_params is provided, then they all need to be provided
z_params = (self.z_stage_name, self.z_start, self.z_end, self.z_step)
if any(z_params) and None in z_params:
raise TypeError(
'All of z_stage_name, z_start_, z_end, and z_step must be provided'
)

if self.z_step is not None:
self.z_range = list(np.arange(self.z_start, self.z_end + self.z_step, self.z_step))
self.num_slices = len(self.z_range)


@dataclass
@dataclass(config=config)
class MicroscopeSettings:
roi: Optional[Tuple[int, int, int, int]] = None
config_group_settings: List[ConfigSettings] = field(default_factory=list)
Expand Down
34 changes: 15 additions & 19 deletions mantis/acquisition/acq_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from dataclasses import asdict
from datetime import datetime
from functools import partial
from typing import Iterable
from pathlib import Path
from typing import Iterable, Union

import nidaqmx
import numpy as np
Expand Down Expand Up @@ -239,7 +240,7 @@ class MantisAcquisition(object):
Parameters
----------
acquisition_directory : str
acquisition_directory : str or PathLike
Directory where acquired data will be saved
acquisition_name : str
Name of the acquisition
Expand Down Expand Up @@ -276,7 +277,7 @@ class MantisAcquisition(object):

def __init__(
self,
acquisition_directory: str,
acquisition_directory: Union[str, os.PathLike],
acquisition_name: str,
mm_app_path: str = r'C:\\Program Files\\Micro-Manager-nightly',
mm_config_file: str = r'C:\\CompMicro_MMConfigs\\mantis\\mantis-LS.cfg',
Expand All @@ -285,8 +286,7 @@ def __init__(
demo_run: bool = False,
verbose: bool = False,
) -> None:

self._root_dir = acquisition_directory
self._root_dir = Path(acquisition_directory).resolve()
self._acq_name = acquisition_name
self._demo_run = demo_run
self._verbose = verbose
Expand All @@ -296,12 +296,12 @@ def __init__(

# Create acquisition directory and log directory
self._acq_dir = _create_acquisition_directory(self._root_dir, self._acq_name)
self._logs_dir = os.path.join(self._acq_dir, 'logs')
os.mkdir(self._logs_dir)
self._logs_dir = self._acq_dir / 'logs'
self._logs_dir.mkdir()

# Setup logger
timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
acq_log_path = os.path.join(self._logs_dir, f'mantis_acquisition_log_{timestamp}.txt')
acq_log_path = self._logs_dir / f'mantis_acquisition_log_{timestamp}.txt'
configure_logger(acq_log_path)

if self._demo_run:
Expand All @@ -310,7 +310,7 @@ def __init__(

# Log conda environment
outs, errs = log_conda_environment(
os.path.join(self._logs_dir, f'conda_environment_log_{timestamp}.txt')
self._logs_dir / f'conda_environment_log_{timestamp}.txt'
)
if errs is None:
logger.debug(outs.decode('ascii').strip())
Expand All @@ -333,9 +333,7 @@ def __init__(
mm_app_path=mm_app_path,
mm_config_file=mm_config_file,
zmq_port=LS_ZMQ_PORT,
core_log_path=os.path.join(
mm_app_path, 'CoreLogs', f'CoreLog{timestamp}_headless.txt'
),
core_log_path=Path(mm_app_path) / 'CoreLogs' / f'CoreLog{timestamp}_headless.txt',
)

@property
Expand Down Expand Up @@ -729,7 +727,7 @@ def refocus_ls_path(self):
# Save acquired stacks in logs
timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
tifffile.imwrite(
os.path.join(self._logs_dir, f'ls_refocus_data_{timestamp}.ome.tif'),
self._logs_dir / f'ls_refocus_data_{timestamp}.ome.tif',
np.expand_dims(data, -3).astype('uint16'),
)

Expand All @@ -747,9 +745,7 @@ def refocus_ls_path(self):
lambda_ill=wavelength,
pixel_size=LS_PIXEL_SIZE,
threshold_FWHM=threshold_FWHM,
plot_path=os.path.join(
self._logs_dir, f'ls_refocus_plot_{timestamp}_Pos{stack_idx}.png'
),
plot_path=self._logs_dir / f'ls_refocus_plot_{timestamp}_Pos{stack_idx}.png',
)
focus_indices.append(idx)
logger.debug(
Expand Down Expand Up @@ -1001,10 +997,10 @@ def _generate_channel_slice_acq_events(channel_settings, slice_settings):
return events


def _create_acquisition_directory(root_dir, acq_name, idx=1):
acq_dir = os.path.join(root_dir, f'{acq_name}_{idx}')
def _create_acquisition_directory(root_dir: Path, acq_name: str, idx=1) -> Path:
acq_dir = root_dir / f'{acq_name}_{idx}'
try:
os.mkdir(acq_dir)
acq_dir.mkdir(parents=False, exist_ok=False)
except OSError:
return _create_acquisition_directory(root_dir, acq_name, idx + 1)
return acq_dir
9 changes: 6 additions & 3 deletions mantis/acquisition/logger.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging
import os

from pathlib import Path
from subprocess import PIPE, STDOUT, Popen
from typing import Union


def log_conda_environment(log_path: str):
def log_conda_environment(log_path: Union[str, os.PathLike]):
"""Save a log of the current conda environment as defined in the
`compmicro-docs/hpc/log_environment.ps1` PowerShell script. A copy of the
script is kept in a separate location, given in `log_script_path` to avoid
Expand All @@ -13,7 +15,7 @@ def log_conda_environment(log_path: str):
Parameters
----------
log_path : str
log_path : str or PathLike
Path where the environment log file will be written
Returns
Expand All @@ -23,12 +25,13 @@ def log_conda_environment(log_path: str):
"""

# Validate log_path input
log_path = str(Path(log_path).absolute())
assert log_path.endswith('.txt'), 'Log path must point to a .txt file'

# get current conda environment
conda_environment = os.environ['CONDA_DEFAULT_ENV']
# define absolute path to log_environment.ps1 script
log_script_path = 'C:\\Users\\labelfree\\log_environment.ps1'
log_script_path = str(Path.home() / "log_environment.ps1")

# `pwsh` command launches PowerShell 7. Do not use `powershell` as it
# launches PowerShell 6 which is not configured with conda
Expand Down
11 changes: 7 additions & 4 deletions mantis/acquisition/settings/example_acquisition_settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ lf_slice_settings:
z_start: -10
z_end: 10
# When using 0.52 NA transmission light illumination, Nyquist sampling of the
# axial dimension will be achieved by using (450*1.4)/(1.35^2 + 0.52^2) = 300 nm
# axial dimension will be achieved by using (450*1.4)/(1.35^2 + 0.52^2) = 300 nm
# steps in sample space. We can achieve this by using (300 nm)*1.4/2 = 210 nm
# steps of the mirror due to the 1.4x magnification of the remote volume and the
# fact that the optical path length is extended by twice the mirror's motion.
#
# steps of the mirror due to the 1.4x magnification of the remote volume and the
# fact that the optical path length is extended by twice the mirror's motion.
#
# Here we choose to oversample the data to match the light-sheet axial
# sampling. After deskewing and 3x binning, the light-sheet samples are
# spaced by ~205 nm, so we choose (205 nm)*1.4/2 = 143 nm mirror steps.
Expand Down Expand Up @@ -126,3 +126,6 @@ ls_microscope_settings:
- ['TS2_TTL1-8', 'Blanking', 'Off']
z_sequencing_settings:
- [*AP_GALVO_DAC, 'Sequence', 'On']
use_o3_refocus: True
o3_refocus_config: ['Channel - LS', 'GFP EX488 EM525-45']
o3_refocus_interval_min: 10
22 changes: 14 additions & 8 deletions mantis/analysis/AnalysisSettings.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
from pydantic import validator
from pydantic.dataclasses import dataclass
from typing import Optional

import numpy as np

from pydantic import ConfigDict, PositiveFloat, PositiveInt, validator
from pydantic.dataclasses import dataclass

config = ConfigDict(extra='forbid')

@dataclass

@dataclass(config=config)
class DeskewSettings:
pixel_size_um: float
ls_angle_deg: float
px_to_scan_ratio: float = None
scan_step_um: float = None
pixel_size_um: PositiveFloat
ls_angle_deg: PositiveFloat
px_to_scan_ratio: Optional[PositiveFloat] = None
scan_step_um: Optional[PositiveFloat] = None
keep_overhang: bool = True
average_n_slices: PositiveInt = 3

@validator("ls_angle_deg")
def ls_angle_check(cls, v):
Expand All @@ -27,7 +33,7 @@ def __post_init__(self):
if self.scan_step_um is not None:
self.px_to_scan_ratio = round(self.pixel_size_um / self.scan_step_um, 3)
else:
raise Exception("px_to_scan_ratio is not valid")
raise TypeError("px_to_scan_ratio is not valid")


@dataclass
Expand Down
Loading

0 comments on commit 1a1eb2e

Please sign in to comment.