Single line summaries of docstrings
This commit is contained in:
parent
35d47fe3ed
commit
4dc41bb008
20 changed files with 153 additions and 115 deletions
|
|
@ -1,8 +1,5 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
|
"""Check that the npm version string matches the Python version string exactly."""
|
||||||
"""A script to check that the npm version string matches the python
|
|
||||||
version string exactly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ def test_calibration(client):
|
||||||
|
|
||||||
|
|
||||||
def test_jpeg_and_array(client):
|
def test_jpeg_and_array(client):
|
||||||
"""Check that grabbing a jpeg from the stream results in the same size
|
"""Check that a jpeg grabbed from the stream is the same size as other captures.
|
||||||
image as a array capture or a jpeg capture.
|
|
||||||
|
Compare it to an array capture and a jpeg capture.
|
||||||
"""
|
"""
|
||||||
# Grab a jpeg from the stream
|
# Grab a jpeg from the stream
|
||||||
blob = client.grab_jpeg()
|
blob = client.grab_jpeg()
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ def print_tuning(read_file: bool = False):
|
||||||
|
|
||||||
|
|
||||||
def _test_bad_tuning_after_good_tuning(configure: bool = False):
|
def _test_bad_tuning_after_good_tuning(configure: bool = False):
|
||||||
"""Load the default tuning file into the camera, re-load with a broken tuning file,
|
"""Test loading good, bad, then another good tuning files in sequence.
|
||||||
|
|
||||||
|
Load the default tuning file into the camera, re-load with a broken tuning file,
|
||||||
check it errors. Finally check the default tuning file will load again afterwards.
|
check it errors. Finally check the default tuning file will load again afterwards.
|
||||||
|
|
||||||
:param configure: Boolean, set true to configure the camera on initial loading
|
:param configure: Boolean, set true to configure the camera on initial loading
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,9 @@ ignore = [
|
||||||
"E501", # Ignore long lines for now
|
"E501", # Ignore long lines for now
|
||||||
"D203", # incompatible with D204
|
"D203", # incompatible with D204
|
||||||
"D213", # incompatible with D212
|
"D213", # incompatible with D212
|
||||||
|
"D400", # A stricter version of #415
|
||||||
# Below are checkers to be turned on gradually
|
# Below are checkers to be turned on gradually
|
||||||
"D205", # Force single line summary!
|
|
||||||
"D415",
|
"D415",
|
||||||
"D400",
|
|
||||||
"D200",
|
"D200",
|
||||||
# These should be turned on before this MR is complete
|
# These should be turned on before this MR is complete
|
||||||
"D100",
|
"D100",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"""Copyright (c) 2020 Bath Open Instrumentation Group
|
"""A script for uploading to zenodo.
|
||||||
|
|
||||||
|
Copyright (c) 2020 Bath Open Instrumentation Group
|
||||||
Adapted from: https://gitlab.com/schlauch/zenodo-api-test/
|
Adapted from: https://gitlab.com/schlauch/zenodo-api-test/
|
||||||
Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved.
|
Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved.
|
||||||
SPDX-License-Identifier: MIT-DLR
|
SPDX-License-Identifier: MIT-DLR
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,7 @@ def retrieve_log_from_file() -> PlainTextResponse:
|
||||||
|
|
||||||
|
|
||||||
class OFMHandler(logging.Handler):
|
class OFMHandler(logging.Handler):
|
||||||
"""A child class of logging.Handler. This class handles storing the most recent
|
"""A logging.Handler that stores the most recent logs for access by the server."""
|
||||||
logs for access by the server.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, level=logging.INFO, max_logs=250):
|
def __init__(self, level=logging.INFO, max_logs=250):
|
||||||
super().__init__(level=level)
|
super().__init__(level=level)
|
||||||
|
|
@ -78,8 +76,11 @@ class OFMHandler(logging.Handler):
|
||||||
self._max_logs = max_logs
|
self._max_logs = max_logs
|
||||||
|
|
||||||
def append_record(self, record):
|
def append_record(self, record):
|
||||||
"""Use the built in formatter to format the record, then save
|
"""Format message and append it to a list of records.
|
||||||
it to an array. Pop any in excess of the maximum number of logs
|
|
||||||
|
The built in formatter is used to format the record.
|
||||||
|
|
||||||
|
Any records in excess of the maximum number of logs are removed.
|
||||||
"""
|
"""
|
||||||
self._log.append(self.format(record))
|
self._log.append(self.format(record))
|
||||||
while len(self._log) > self._max_logs:
|
while len(self._log) > self._max_logs:
|
||||||
|
|
|
||||||
|
|
@ -224,8 +224,9 @@ class ScanDirectory:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def images_dir(self) -> Optional[str]:
|
def images_dir(self) -> Optional[str]:
|
||||||
"""The path to the images directory. None is returned if no images directory
|
"""The path to the images directory.
|
||||||
was created
|
|
||||||
|
None is returned if no images directory was created.
|
||||||
"""
|
"""
|
||||||
im_path = os.path.join(self.dir_path, IMG_DIR_NAME)
|
im_path = os.path.join(self.dir_path, IMG_DIR_NAME)
|
||||||
if os.path.isdir(im_path):
|
if os.path.isdir(im_path):
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,7 @@ class ScanPlanner:
|
||||||
return tuple(position) in self._remaining_locations
|
return tuple(position) in self._remaining_locations
|
||||||
|
|
||||||
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
|
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
|
||||||
"""Return the next location to scan, and the estimated z-position
|
"""Return the next location to scan and its estimated z-position.
|
||||||
for this location.
|
|
||||||
|
|
||||||
Note z-position may be None! This indicates that the current z, position
|
Note z-position may be None! This indicates that the current z, position
|
||||||
should be used.
|
should be used.
|
||||||
|
|
@ -176,9 +175,12 @@ class ScanPlanner:
|
||||||
return next_location, z
|
return next_location, z
|
||||||
|
|
||||||
def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
|
def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
|
||||||
"""Return the xyz position of the closest site where focus was achieved
|
"""Return the xyz position of the closest site where focus was achieved.
|
||||||
to the input xy_position, with the most recently taken image returned in
|
|
||||||
the case of a tie
|
The most recently taken image is returned in the case of a tie.
|
||||||
|
|
||||||
|
:param xy_pos: The xy_position which the returned position should be closest
|
||||||
|
to.
|
||||||
|
|
||||||
Returns None if there if no focussed locations are present
|
Returns None if there if no focussed locations are present
|
||||||
"""
|
"""
|
||||||
|
|
@ -338,9 +340,10 @@ class SmartSpiral(ScanPlanner):
|
||||||
self._remaining_locations.sort(key=sort_key)
|
self._remaining_locations.sort(key=sort_key)
|
||||||
|
|
||||||
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
|
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
|
||||||
"""Return the next location to scan, and the estimated z-position
|
"""Return the next location to scan and its estimated z-position.
|
||||||
for this location. This overrides the default behaviour of ScanPlanner
|
|
||||||
to take the lowest value of nearest neighbours as this works best for smart stack
|
This overrides the default behaviour of ScanPlanner to take the lowest value of
|
||||||
|
nearest neighbours as this works best for smart stack.
|
||||||
|
|
||||||
Note z-position may be None! This indicates that the current z position
|
Note z-position may be None! This indicates that the current z position
|
||||||
should be used.
|
should be used.
|
||||||
|
|
@ -361,9 +364,11 @@ class SmartSpiral(ScanPlanner):
|
||||||
|
|
||||||
def select_nearby_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
|
def select_nearby_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
|
||||||
"""Return the xyz position of the nearby site with the lowest z position.
|
"""Return the xyz position of the nearby site with the lowest z position.
|
||||||
|
|
||||||
Lowest position is best, as starting too high causes smart stacking to
|
Lowest position is best, as starting too high causes smart stacking to
|
||||||
autofocus and restart. Starting too low just requires extra movements in +z.
|
autofocus and restart. Starting too low just requires extra movements in +z.
|
||||||
Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest neighbour.
|
Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest
|
||||||
|
neighbour.
|
||||||
|
|
||||||
Returns None if there if no focused locations are present
|
Returns None if there if no focused locations are present
|
||||||
"""
|
"""
|
||||||
|
|
@ -397,12 +402,10 @@ class SmartSpiral(ScanPlanner):
|
||||||
starting_pos: XYPos | np.ndarray,
|
starting_pos: XYPos | np.ndarray,
|
||||||
ending_pos: XYPos | np.ndarray,
|
ending_pos: XYPos | np.ndarray,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Return the number of moves between two xy positions in the x or y direction
|
"""Return the larger of x moves or y moves between two xy positions.
|
||||||
whichever is largest
|
|
||||||
|
|
||||||
Args:
|
:param starting_pos: the position to measure from
|
||||||
starting_pos: the position to measure from
|
:param ending_pos: the position to measure to
|
||||||
ending_pos: the position to measure to
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
move_size = np.array([self._dx, self._dy])
|
move_size = np.array([self._dx, self._dy])
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
def add_static_file(app: FastAPI, fname: str, folder: str) -> None:
|
def add_static_file(app: FastAPI, fname: str, folder: str) -> None:
|
||||||
"""Add a single file to the root of the FastAPI app
|
"""Add a single file to the root of the FastAPI app.
|
||||||
|
|
||||||
The file with name ``fname`` will be mounted at ``/fname`` - the
|
The file with name ``fname`` will be mounted at ``/fname`` - the
|
||||||
``folder`` does not affect where it is mounted in the app.
|
``folder`` does not affect where it is mounted in the app.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,7 @@ def _get_capture_by_id(captures: list[CaptureInfo], buffer_id: int) -> CaptureIn
|
||||||
|
|
||||||
|
|
||||||
def _get_capture_index_by_id(captures: list[CaptureInfo], buffer_id: int) -> int:
|
def _get_capture_index_by_id(captures: list[CaptureInfo], buffer_id: int) -> int:
|
||||||
"""Return the index of the capture with the matching id from a list of CaptureInfo
|
"""Return the index of the capture with the matching id.
|
||||||
objects
|
|
||||||
|
|
||||||
:param captures: A list of capture objects
|
:param captures: A list of capture objects
|
||||||
:param buffer_id: The buffer id of the image to return
|
:param buffer_id: The buffer id of the image to return
|
||||||
|
|
@ -432,10 +431,13 @@ class AutofocusThing(lt.Thing):
|
||||||
autofocus_dz: int,
|
autofocus_dz: int,
|
||||||
save_resolution: tuple[int, int],
|
save_resolution: tuple[int, int],
|
||||||
) -> tuple[bool, int]:
|
) -> tuple[bool, int]:
|
||||||
"""Run a smart stack, which captures images offset in z, testing
|
"""Run a smart stack.
|
||||||
whether the sharpest image is towards the centre of the stack.
|
|
||||||
The sharpest image, and optionally images around the sharpest,
|
A smart stack captures images offset in z, testing whether the sharpest image
|
||||||
will be saved using their coordinates to images_dir
|
is towards the centre of the stack.
|
||||||
|
|
||||||
|
The sharpest image, and optionally images around the sharpest, will be saved
|
||||||
|
to the images_dir with their coordinates in the filename.
|
||||||
|
|
||||||
|
|
||||||
:param cam: Camera Dependency supplied by LabThings dependency injection
|
:param cam: Camera Dependency supplied by LabThings dependency injection
|
||||||
|
|
@ -506,15 +508,13 @@ class AutofocusThing(lt.Thing):
|
||||||
stage: Stage,
|
stage: Stage,
|
||||||
sharpness_monitor: SharpnessMonitorDep,
|
sharpness_monitor: SharpnessMonitorDep,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Return to the initial height of the current stack, and run
|
"""Return to the initial z position and run a looping autofocus.
|
||||||
a looping autofocus.
|
|
||||||
|
|
||||||
:param initial_z_pos: The initial z positions of previous captures
|
:param initial_z_pos: The initial z positions of previous captures
|
||||||
:param autofocus_dz: the range in steps to autofocus
|
:param autofocus_dz: the range in steps to autofocus
|
||||||
|
|
||||||
``stage`` and ``sharpness_monitor`` are Thing dependencies passed through
|
``stage`` and ``sharpness_monitor`` are Thing dependencies passed through
|
||||||
from the calling action
|
from the calling action.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
stage.move_absolute(z=initial_z_pos)
|
stage.move_absolute(z=initial_z_pos)
|
||||||
self.looping_autofocus(
|
self.looping_autofocus(
|
||||||
|
|
@ -530,8 +530,10 @@ class AutofocusThing(lt.Thing):
|
||||||
stack_parameters: StackParams,
|
stack_parameters: StackParams,
|
||||||
cam: WrappedCamera,
|
cam: WrappedCamera,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Save the required captures to disk. Will save the sharpest image,
|
"""Save the required captures to disk.
|
||||||
and any images either side of focus.
|
|
||||||
|
This will save the sharpest image, and optionally extra images either
|
||||||
|
side of focus (see ``stack_parameters.images_to_save``).
|
||||||
|
|
||||||
:param sharpest_id: the buffer id index of the sharpest image
|
:param sharpest_id: the buffer id index of the sharpest image
|
||||||
:param captures: a list of captures, including file name, image data and
|
:param captures: a list of captures, including file name, image data and
|
||||||
|
|
@ -559,8 +561,12 @@ class AutofocusThing(lt.Thing):
|
||||||
cam: WrappedCamera,
|
cam: WrappedCamera,
|
||||||
stage: Stage,
|
stage: Stage,
|
||||||
) -> tuple[bool, list[CaptureInfo], Optional[int]]:
|
) -> tuple[bool, list[CaptureInfo], Optional[int]]:
|
||||||
"""Capture a series of images offset by stack_parameters.stack_dz, and test whether
|
"""Capture a series of images checking that sharpest image central
|
||||||
the sharpest image is towards the centre of the stack.
|
|
||||||
|
The images are seperated in z offset by stack_parameters.stack_dz, as they
|
||||||
|
are captured the last stack_parameters.min_images_to_test images are checked
|
||||||
|
to see if the sharpest image is central enough in the stack. If it is the stack
|
||||||
|
completes.
|
||||||
|
|
||||||
:param stack_parameters: a StackParams object holding stack parameters
|
:param stack_parameters: a StackParams object holding stack parameters
|
||||||
:param cam: Camera Dependency to be passed through from the calling action
|
:param cam: Camera Dependency to be passed through from the calling action
|
||||||
|
|
@ -644,8 +650,7 @@ class AutofocusThing(lt.Thing):
|
||||||
def check_stack_result(
|
def check_stack_result(
|
||||||
self, captures: list[CaptureInfo]
|
self, captures: list[CaptureInfo]
|
||||||
) -> tuple[Literal["success", "continue", "restart"], int]:
|
) -> tuple[Literal["success", "continue", "restart"], int]:
|
||||||
"""Test a list of captures, to decide whether the sharpest image from a
|
"""Check if the sharpest image in a list of captures is central enough.
|
||||||
stack is centrally enough in the stack
|
|
||||||
|
|
||||||
:param captures: a list of the capture objects to for testing if the
|
:param captures: a list of the capture objects to for testing if the
|
||||||
sharpness has converged in the centre
|
sharpness has converged in the centre
|
||||||
|
|
|
||||||
|
|
@ -156,8 +156,10 @@ class BaseCamera(lt.Thing):
|
||||||
def start_streaming(
|
def start_streaming(
|
||||||
self, main_resolution: tuple[int, int], buffer_count: int
|
self, main_resolution: tuple[int, int], buffer_count: int
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Start (or stop and restart) the camera with the given resolution
|
"""Start (or stop and restart) the camera.
|
||||||
for the main stream, and buffer_count number of images in the buffer
|
|
||||||
|
:param main_resolution: the resolution to use for the main stream.
|
||||||
|
:param buffer_count: number of images in the stream buffer.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"CameraThings must define their own start_streaming method"
|
"CameraThings must define their own start_streaming method"
|
||||||
|
|
@ -342,12 +344,14 @@ class BaseCamera(lt.Thing):
|
||||||
metadata_getter: lt.deps.GetThingStates,
|
metadata_getter: lt.deps.GetThingStates,
|
||||||
logger: lt.deps.InvocationLogger,
|
logger: lt.deps.InvocationLogger,
|
||||||
) -> Image:
|
) -> Image:
|
||||||
"""Capture an image in memory and return it with metadata
|
"""Capture an image in memory and return it with metadata.
|
||||||
CaptureError raised if the capture fails for any reason
|
|
||||||
returns tuple with PIL Image, and dict of metadata.
|
|
||||||
|
|
||||||
This robust capturing method attempts to capture the image five times
|
This robust capturing method attempts to capture the image five times
|
||||||
each time with a 5 second timeout set.
|
each time with a 5 second timeout set.
|
||||||
|
|
||||||
|
:raises CaptureError: if the capture fails for any reason
|
||||||
|
|
||||||
|
:returns: tuple with PIL Image, and dictionary of metadata.
|
||||||
"""
|
"""
|
||||||
for capture_attempts in range(5):
|
for capture_attempts in range(5):
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,9 @@ class PicameraStreamOutput(Output):
|
||||||
|
|
||||||
|
|
||||||
class SensorMode(BaseModel):
|
class SensorMode(BaseModel):
|
||||||
"""A Pydantic model holding all the information about a specific
|
"""A Pydantic model holding all the information about a specific sensor mode.
|
||||||
sensor mode as reported by the PiCamera.
|
|
||||||
|
This data is as reported by the PiCamera2 module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unpacked: str
|
unpacked: str
|
||||||
|
|
@ -76,10 +77,11 @@ class SensorMode(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class SensorModeSelector(BaseModel):
|
class SensorModeSelector(BaseModel):
|
||||||
"""A Pydantic model holding the two values needed to select a PiCamera
|
"""A Pydantic model holding the two values needed to select a PiCamera Sensor mode.
|
||||||
Sensor mode. The output size and the bit depth.
|
|
||||||
|
|
||||||
This is a Pydantic modell so that it can be saved to the disk.
|
Theses values are the output size and the bit depth.
|
||||||
|
|
||||||
|
This is a Pydantic model so that it can be saved to disk.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output_size: tuple[int, int]
|
output_size: tuple[int, int]
|
||||||
|
|
|
||||||
|
|
@ -477,12 +477,16 @@ def set_static_geq(
|
||||||
tuning: dict,
|
tuning: dict,
|
||||||
offset: int = 65535,
|
offset: int = 65535,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update the ``rpi.geq`` section of a camera tuning dict to always use green
|
"""Update the ``rpi.geq`` section of a camera tuning dict.
|
||||||
equalisation that averages the green pixels in the red and blue rows.
|
|
||||||
|
|
||||||
``tuning`` will be updated in-place to set the geq offest to the given value.
|
:param tuning: the raspberry pi tuning file. This will be updated in-place to
|
||||||
The default 65535 is the maximum allowed value. This means
|
set the geq offest to the given value.
|
||||||
the brightness will always be below the threshold where averaging is used.
|
:param offset: The desired green equalisation offset. Default 65535. The default is
|
||||||
|
the maximum allowed value. This means the brightness will always be below the
|
||||||
|
threshold where averaging is used. This is default as we always need the green
|
||||||
|
equalisation to averages the green pixels in the red and blue rows due to the
|
||||||
|
chief ray angle compensation issue when the the stock lens is replaced by an
|
||||||
|
objective.
|
||||||
"""
|
"""
|
||||||
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
|
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
|
||||||
geq["offset"] = offset # max out offset to disable the adaptive green equalisation
|
geq["offset"] = offset # max out offset to disable the adaptive green equalisation
|
||||||
|
|
|
||||||
|
|
@ -311,8 +311,9 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _save_scan_inputs_json(self):
|
def _save_scan_inputs_json(self):
|
||||||
"""Save scan inputs as a JSON file in the scan folder, to allow
|
"""Save scan inputs as a JSON file in the scan folder.
|
||||||
the user to review the settings used in the scan
|
|
||||||
|
This file allows the user to review the settings used in the scan.
|
||||||
"""
|
"""
|
||||||
# Should this be a method of the scan_data dataclass?
|
# Should this be a method of the scan_data dataclass?
|
||||||
|
|
||||||
|
|
@ -335,11 +336,10 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _update_scan_data_json(self, scan_result: str):
|
def _update_scan_data_json(self, scan_result: str):
|
||||||
"""Update scan data as a JSON file in the scan folder, with
|
"""Update scan data JSON file with data only known at the end of the scan.
|
||||||
data only known at the end of the scan.
|
|
||||||
|
|
||||||
Takes scan_result, a string that is either "success",
|
Takes scan_result, a string that is either "success", "cancelled by user",
|
||||||
"cancelled by user", or the error that ended the scan.
|
or the error that ended the scan.
|
||||||
"""
|
"""
|
||||||
# Should this be a method of the scan_data dataclass?
|
# Should this be a method of the scan_data dataclass?
|
||||||
current_time = datetime.now().replace(microsecond=0)
|
current_time = datetime.now().replace(microsecond=0)
|
||||||
|
|
@ -378,10 +378,11 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _run_scan(self):
|
def _run_scan(self):
|
||||||
"""Prepare and run the main scan, handling the threads performing
|
"""Prepare and run the main scan, and perform final actions on completion.
|
||||||
stitching. Uses the result (or exception) from the scanning to
|
|
||||||
determine whether the scan should be stitched and the microscope
|
The result (or exception) from the main scan loop determines whether the
|
||||||
should return to the starting x,y,z position
|
scan should be stitched and whether the microscope should return to the
|
||||||
|
starting x,y,z position.
|
||||||
"""
|
"""
|
||||||
# Used to check if finally was reached via exception (except
|
# Used to check if finally was reached via exception (except
|
||||||
# cancel by user)
|
# cancel by user)
|
||||||
|
|
@ -765,8 +766,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _preview_stitch_start(self, overlap: float) -> None:
|
def _preview_stitch_start(self, overlap: float) -> None:
|
||||||
"""Start stitching a preview of the scan in a background subprocess
|
"""Start stitching a preview of the scan in a background subprocess.
|
||||||
|
|
||||||
|
|
||||||
This uses popen and returns immediately
|
This uses popen and returns immediately
|
||||||
|
|
||||||
|
|
@ -954,8 +954,9 @@ class SmartScanThing(lt.Thing):
|
||||||
self,
|
self,
|
||||||
scan_name: str,
|
scan_name: str,
|
||||||
):
|
):
|
||||||
"""Update the zip to include the files left until the end, then return the
|
"""Return zip after including any files left until the end.
|
||||||
zip file as a Blob
|
|
||||||
|
The zipfile is returned as a Blob.
|
||||||
"""
|
"""
|
||||||
zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True)
|
zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True)
|
||||||
return ZipBlob.from_file(zip_fname)
|
return ZipBlob.from_file(zip_fname)
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,10 @@ class ErrorCapturingThread(Thread):
|
||||||
)
|
)
|
||||||
|
|
||||||
def join(self, timeout=None):
|
def join(self, timeout=None):
|
||||||
"""Join when the thread is complete. If the thread ended due to an unhandled exception,
|
"""Join when the thread is complete.
|
||||||
the exception will be raised when this method is called.
|
|
||||||
|
If the thread ended due to an unhandled exception, the exception will be raised
|
||||||
|
when this method is called.
|
||||||
"""
|
"""
|
||||||
super().join(timeout)
|
super().join(timeout)
|
||||||
# If there is an error in the error buffer clear the buffer and raise it
|
# If there is an error in the error buffer clear the buffer and raise it
|
||||||
|
|
|
||||||
|
|
@ -183,9 +183,7 @@ def test_scan_sequence_and_listing():
|
||||||
|
|
||||||
|
|
||||||
def test_scan_name_non_sequential():
|
def test_scan_name_non_sequential():
|
||||||
"""Check created scans is the correct name if the directories
|
"""Check new scan has the correct name if the directories are not sequential"""
|
||||||
are not sequential
|
|
||||||
"""
|
|
||||||
_clear_scan_dir()
|
_clear_scan_dir()
|
||||||
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0001"))
|
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0001"))
|
||||||
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0002"))
|
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0002"))
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,10 @@ def test_mark_wrong_location():
|
||||||
|
|
||||||
|
|
||||||
def test_closest_focus_wth_large_numbers():
|
def test_closest_focus_wth_large_numbers():
|
||||||
"""The number of steps gets very large in reality runs some tests to check
|
"""Tests to check that everything works well with huge numbers of steps.
|
||||||
that everything works well with huge numbers of steps
|
|
||||||
|
The number of steps gets very large on the micorscope. But most of the tests
|
||||||
|
above use smaller numbers for clarity.
|
||||||
"""
|
"""
|
||||||
intial_position = (0, 0)
|
intial_position = (0, 0)
|
||||||
# Set this up, but we won't use the settings
|
# Set this up, but we won't use the settings
|
||||||
|
|
@ -247,11 +249,18 @@ def test_closest_focus_wth_large_numbers():
|
||||||
|
|
||||||
|
|
||||||
def test_example_smart_spiral():
|
def test_example_smart_spiral():
|
||||||
"""Test the smart spiral scan algorithm on the sample types listed
|
"""Test the smart spiral scan algorithm on the different sample types.
|
||||||
below and defined in scan_test_helpers.load_sample_points
|
|
||||||
|
|
||||||
Will fail if the locations or path between locations visited has changed
|
The sample types:
|
||||||
for any of the samples listed
|
|
||||||
|
* ``regular``
|
||||||
|
* ``lobed``
|
||||||
|
* ``core"``
|
||||||
|
|
||||||
|
These are defined in scan_test_helpers.load_sample_points
|
||||||
|
|
||||||
|
This will fail if the locations or path between locations visited has changed
|
||||||
|
for any of the samples listed.
|
||||||
"""
|
"""
|
||||||
example_samples = [
|
example_samples = [
|
||||||
"regular",
|
"regular",
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,10 @@ def test_initial_properties(smart_scan_thing):
|
||||||
|
|
||||||
|
|
||||||
def test_inaccessible_scan_methods(smart_scan_thing):
|
def test_inaccessible_scan_methods(smart_scan_thing):
|
||||||
"""The @_scan_running decorator should make some methods
|
"""Test that method with @_scan_running decorator is inaccessible.
|
||||||
inaccessible unless a scan is running
|
|
||||||
|
The @_scan_running decorator makes these functions inacessible unless
|
||||||
|
a scan is running.
|
||||||
"""
|
"""
|
||||||
with pytest.raises(ScanNotRunningError):
|
with pytest.raises(ScanNotRunningError):
|
||||||
smart_scan_thing._run_scan()
|
smart_scan_thing._run_scan()
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ def even_integers(min_value=0, max_value=1000):
|
||||||
extra_ims=even_integers(min_value=0, max_value=10),
|
extra_ims=even_integers(min_value=0, max_value=10),
|
||||||
)
|
)
|
||||||
def test_stack_params_validation(save_ims, extra_ims):
|
def test_stack_params_validation(save_ims, extra_ims):
|
||||||
"""Tests specifically the validation on the image numbers
|
"""Test the validation of the image numbers for a stack.
|
||||||
|
|
||||||
save_ims is the number to save (must be odd and positive)
|
save_ims is the number to save (must be odd and positive)
|
||||||
extra_ims is how many more images there are in min_images_to_test than
|
extra_ims is how many more images there are in min_images_to_test than
|
||||||
|
|
@ -68,8 +68,9 @@ def test_stack_params_validation(save_ims, extra_ims):
|
||||||
extra_ims=even_integers(min_value=-10, max_value=-1),
|
extra_ims=even_integers(min_value=-10, max_value=-1),
|
||||||
)
|
)
|
||||||
def test_stack_params_not_enough_test_images(save_ims, extra_ims):
|
def test_stack_params_not_enough_test_images(save_ims, extra_ims):
|
||||||
"""Set the extra_ims negative so that min_images_to_test is smaller
|
"""Test error is raised if min_images_to_test is smaller than images_to_save.
|
||||||
than images_to_save.
|
|
||||||
|
``extra_ims`` negative so that min_images_to_test is smaller than images_to_save.
|
||||||
|
|
||||||
For arguments see test_stack_params_validation
|
For arguments see test_stack_params_validation
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -14,23 +14,23 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
|
||||||
class FakeSample:
|
class FakeSample:
|
||||||
"""A fake sample to test scan algorithms. The sample is able to return
|
"""A fake sample to test scan algorithms.
|
||||||
whether a given position is sample, no image associated with the sample
|
|
||||||
|
The sample is able to return whether a given position is sample, there is no image
|
||||||
|
associated with the sample
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xy_points: list[tuple[int, int]]):
|
def __init__(self, xy_points: list[tuple[int, int]]):
|
||||||
"""Create the sample from a spline interpolation around
|
"""Create the sample from a spline interpolation around the given points."""
|
||||||
the given points.
|
|
||||||
"""
|
|
||||||
self._sample_perimeter = interp_closed_path(xy_points, 500)
|
self._sample_perimeter = interp_closed_path(xy_points, 500)
|
||||||
|
|
||||||
def is_sample(self, pos: tuple[int, int], im_size: tuple[int, int]) -> bool:
|
def is_sample(self, pos: tuple[int, int], im_size: tuple[int, int]) -> bool:
|
||||||
"""Return whether an image at a given location with a given image size
|
"""Return True if an image at a given location is on the sample.
|
||||||
is on the sample
|
|
||||||
|
|
||||||
This doesn't check the entire image field as this is designed to be used
|
The image size is specified to check if it overlaps the sample. It doesn't
|
||||||
where the fake sample is much larger than the image and has smooth edges
|
check the entire image field as this is designed to be used where the fake
|
||||||
It just checks the 4 corners
|
sample is much larger than the image and has smooth edges. It just checks the
|
||||||
|
4 corners.
|
||||||
"""
|
"""
|
||||||
img_corners = [
|
img_corners = [
|
||||||
(pos[0] + im_size[0], pos[1] + im_size[1]),
|
(pos[0] + im_size[0], pos[1] + im_size[1]),
|
||||||
|
|
@ -77,8 +77,9 @@ def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Fi
|
||||||
|
|
||||||
|
|
||||||
def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPath:
|
def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPath:
|
||||||
"""Given a lists of xy_points interpolate an n_point closed curve. This can be used
|
"""Interpolate an n_point closed curve from a lists of xy_points.
|
||||||
to create an arbitrary sample shape plan a scan.
|
|
||||||
|
This can be used to create an arbitrary sample shape for testing a scan planner.
|
||||||
|
|
||||||
Modified from:
|
Modified from:
|
||||||
https://stackoverflow.com/questions/33962717/interpolating-a-closed-curve-using-scipy
|
https://stackoverflow.com/questions/33962717/interpolating-a-closed-curve-using-scipy
|
||||||
|
|
@ -105,8 +106,9 @@ def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPa
|
||||||
def example_smart_spiral(
|
def example_smart_spiral(
|
||||||
sample_name: str = "lobed",
|
sample_name: str = "lobed",
|
||||||
) -> tuple[FakeSample, scan_planners.ScanPlanner]:
|
) -> tuple[FakeSample, scan_planners.ScanPlanner]:
|
||||||
"""Run an example scan and return the sample scanned and the planner object
|
"""Run an example scan.
|
||||||
after scan is complete
|
|
||||||
|
:returns: The sample scanned and the planner object after scan is complete.
|
||||||
"""
|
"""
|
||||||
xy_sample_points = load_sample_points(sample_name)
|
xy_sample_points = load_sample_points(sample_name)
|
||||||
sample = FakeSample(xy_sample_points)
|
sample = FakeSample(xy_sample_points)
|
||||||
|
|
@ -126,11 +128,11 @@ def example_smart_spiral(
|
||||||
|
|
||||||
|
|
||||||
def profile_and_save_plot_for_example_smart_spiral():
|
def profile_and_save_plot_for_example_smart_spiral():
|
||||||
"""Run the example scan and save a plot and the profile data
|
"""Run the example scan and save a plot and the profile data.
|
||||||
Also print the cumulative stats
|
|
||||||
|
|
||||||
|
Also print the cumulative stats.
|
||||||
|
|
||||||
This runs if you run this file directly
|
This runs if you run this file directly.
|
||||||
"""
|
"""
|
||||||
import pstats
|
import pstats
|
||||||
import cProfile
|
import cProfile
|
||||||
|
|
@ -152,8 +154,9 @@ def profile_and_save_plot_for_example_smart_spiral():
|
||||||
|
|
||||||
|
|
||||||
def update_example_smart_spiral_pickle(sample_name: str):
|
def update_example_smart_spiral_pickle(sample_name: str):
|
||||||
"""Pickle the ScanPlanner for the example_smart_spiral(),
|
"""Pickle the ScanPlanner for the example_smart_spiral().
|
||||||
this is done so the history can be compared by testing to check
|
|
||||||
|
This is done so the history can be compared by testing to check
|
||||||
the algorithm is unchanged.
|
the algorithm is unchanged.
|
||||||
|
|
||||||
If the algorithm is purposefully changed then this will need to be
|
If the algorithm is purposefully changed then this will need to be
|
||||||
|
|
@ -171,8 +174,9 @@ def update_example_smart_spiral_pickle(sample_name: str):
|
||||||
def get_expected_result_for_example_smart_spiral(
|
def get_expected_result_for_example_smart_spiral(
|
||||||
sample_name: str,
|
sample_name: str,
|
||||||
) -> scan_planners.ScanPlanner:
|
) -> scan_planners.ScanPlanner:
|
||||||
"""Return the expected ScanPlanner object for the example_smart_spiral(),
|
"""Return the expected ScanPlanner object for the example_smart_spiral().
|
||||||
this is pickled, so that it can be committed.
|
|
||||||
|
This is loaded from a pickle so that the object can be committed to the repo.
|
||||||
"""
|
"""
|
||||||
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")
|
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")
|
||||||
with open(pkl_fname, "rb") as pkl_file_obj:
|
with open(pkl_fname, "rb") as pkl_file_obj:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue