Starting docstring on the same lines as the quotations
This commit is contained in:
parent
be6a6ca6fe
commit
35d47fe3ed
28 changed files with 92 additions and 214 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
"""
|
"""A script to check that the npm version string matches the python
|
||||||
A script to check that the npm version string matches the python
|
|
||||||
version string exactly.
|
version string exactly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,7 @@ 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 grabbing a jpeg from the stream results in the same size
|
|
||||||
image as a array capture or a jpeg capture.
|
image as a array capture or a jpeg capture.
|
||||||
"""
|
"""
|
||||||
# Grab a jpeg from the stream
|
# Grab a jpeg from the stream
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Check exposure times do not drift.
|
||||||
Check exposure times do not drift.
|
|
||||||
|
|
||||||
This can get very tedious. Recommend running pytest with -s option
|
This can get very tedious. Recommend running pytest with -s option
|
||||||
to monitor progress.
|
to monitor progress.
|
||||||
|
|
@ -19,8 +18,7 @@ logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
def _test_exposure_time_drift(desired_time):
|
def _test_exposure_time_drift(desired_time):
|
||||||
"""
|
"""Capture 10 full resolution images and check that the exposure time remains constant
|
||||||
Capture 10 full resolution images and check that the exposure time remains constant
|
|
||||||
|
|
||||||
This confirms that automatic exposure time adjustment is fully turned off
|
This confirms that automatic exposure time adjustment is fully turned off
|
||||||
"""
|
"""
|
||||||
|
|
@ -64,8 +62,6 @@ def _test_exposure_time_drift(desired_time):
|
||||||
|
|
||||||
|
|
||||||
def test_exposure_time_drift():
|
def test_exposure_time_drift():
|
||||||
"""
|
"""Performs the exposure time test for a range of exposure time values."""
|
||||||
Performs the exposure time test for a range of exposure time values.
|
|
||||||
"""
|
|
||||||
for desired_time in [100, 1000, 10000]:
|
for desired_time in [100, 1000, 10000]:
|
||||||
_test_exposure_time_drift(desired_time)
|
_test_exposure_time_drift(desired_time)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
def test_sensor_mode():
|
def test_sensor_mode():
|
||||||
"""
|
"""Test capturing raw arrays in two different sensor modes"""
|
||||||
Test capturing raw arrays in two different sensor modes
|
|
||||||
"""
|
|
||||||
cam = StreamingPiCamera2()
|
cam = StreamingPiCamera2()
|
||||||
server = ThingServer()
|
server = ThingServer()
|
||||||
server.add_thing(cam, "/camera/")
|
server.add_thing(cam, "/camera/")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
"""
|
"""Tests that check that a tuning file can be reloaded"""
|
||||||
Tests that check that a tuning file can be reloaded
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
@ -16,17 +14,13 @@ MODEL = Picamera2.global_camera_info()[0]["Model"]
|
||||||
|
|
||||||
|
|
||||||
def load_default_tuning():
|
def load_default_tuning():
|
||||||
"""
|
"""Return the default tuning file for the connected camera."""
|
||||||
Return the default tuning file for the connected camera.
|
|
||||||
"""
|
|
||||||
fname = f"{MODEL}.json"
|
fname = f"{MODEL}.json"
|
||||||
return Picamera2.load_tuning_file(fname)
|
return Picamera2.load_tuning_file(fname)
|
||||||
|
|
||||||
|
|
||||||
def generate_bad_tuning():
|
def generate_bad_tuning():
|
||||||
"""
|
"""Return a tuning file with an invalid version number to force an error when loaded."""
|
||||||
Return a tuning file with an invalid version number to force an error when loaded.
|
|
||||||
"""
|
|
||||||
default_tuning = load_default_tuning()
|
default_tuning = load_default_tuning()
|
||||||
bad_tuning = default_tuning.copy()
|
bad_tuning = default_tuning.copy()
|
||||||
bad_tuning["version"] = 999
|
bad_tuning["version"] = 999
|
||||||
|
|
@ -34,8 +28,7 @@ def generate_bad_tuning():
|
||||||
|
|
||||||
|
|
||||||
def print_tuning(read_file: bool = False):
|
def print_tuning(read_file: bool = False):
|
||||||
"""
|
"""Print the path of the default tuning file from the the environment variable.
|
||||||
Print the path of the default tuning file from the the environment variable.
|
|
||||||
|
|
||||||
:param read_file: Boolean, set true to also print the file contents.
|
:param read_file: Boolean, set true to also print the file contents.
|
||||||
|
|
||||||
|
|
@ -53,8 +46,7 @@ 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,
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,7 @@ def set_up_working_dir() -> None:
|
||||||
|
|
||||||
|
|
||||||
def start_server() -> subprocess.Popen:
|
def start_server() -> subprocess.Popen:
|
||||||
"""
|
"""Start the server in a subprocess.
|
||||||
Start the server in a subprocess.
|
|
||||||
|
|
||||||
The server is started in a subprocess and all outputs are buffered.
|
The server is started in a subprocess and all outputs are buffered.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,8 +128,7 @@ ignore = [
|
||||||
"D203", # incompatible with D204
|
"D203", # incompatible with D204
|
||||||
"D213", # incompatible with D212
|
"D213", # incompatible with D212
|
||||||
# Below are checkers to be turned on gradually
|
# Below are checkers to be turned on gradually
|
||||||
"D212",
|
"D205", # Force single line summary!
|
||||||
"D205",
|
|
||||||
"D415",
|
"D415",
|
||||||
"D400",
|
"D400",
|
||||||
"D200",
|
"D200",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Copyright (c) 2020 Bath Open Instrumentation Group
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -68,8 +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 child class of logging.Handler. This class handles storing the most recent
|
|
||||||
logs for access by the server.
|
logs for access by the server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -79,8 +78,7 @@ 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
|
||||||
Use the built in formatter to format the record, then save
|
|
||||||
it to an array. Pop any in excess of the maximum number of logs
|
it to an array. Pop any in excess of the maximum number of logs
|
||||||
"""
|
"""
|
||||||
self._log.append(self.format(record))
|
self._log.append(self.format(record))
|
||||||
|
|
@ -88,9 +86,7 @@ class OFMHandler(logging.Handler):
|
||||||
self._log.pop(0)
|
self._log.pop(0)
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
"""
|
"""Emit will save the logged record to the log"""
|
||||||
Emit will save the logged record to the log
|
|
||||||
"""
|
|
||||||
# Basic filter for now that simply stops uvicorn.access logs
|
# Basic filter for now that simply stops uvicorn.access logs
|
||||||
# These are the logs each time an API endpoint is accessed
|
# These are the logs each time an API endpoint is accessed
|
||||||
# This is only the log for the UI.
|
# This is only the log for the UI.
|
||||||
|
|
@ -108,9 +104,7 @@ class OFMHandler(logging.Handler):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_history(self):
|
def log_history(self):
|
||||||
"""
|
"""Return the log history up to the maximum number of logs"""
|
||||||
Return the log history up to the maximum number of logs
|
|
||||||
"""
|
|
||||||
return "\n".join(self._log)
|
return "\n".join(self._log)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,7 @@ class ScanInfo(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ScanDirectoryManager:
|
class ScanDirectoryManager:
|
||||||
"""
|
"""A class for managing interactions with scan directories"""
|
||||||
A class for managing interactions with scan directories
|
|
||||||
"""
|
|
||||||
|
|
||||||
_base_scan_dir: str
|
_base_scan_dir: str
|
||||||
|
|
||||||
|
|
@ -180,8 +178,7 @@ class ScanDirectoryManager:
|
||||||
return ScanDirectory(scan_name, self.base_dir).zip_files(final_version)
|
return ScanDirectory(scan_name, self.base_dir).zip_files(final_version)
|
||||||
|
|
||||||
def check_free_disk_space(self, min_space: int = 500000000) -> None:
|
def check_free_disk_space(self, min_space: int = 500000000) -> None:
|
||||||
"""
|
"""Raise an exception if there is not enough free disk space to continue scanning
|
||||||
Raise an exception if there is not enough free disk space to continue scanning
|
|
||||||
|
|
||||||
:param min_space: the minimum space required in bytes.
|
:param min_space: the minimum space required in bytes.
|
||||||
Default = 500,000,000 (500MB)
|
Default = 500,000,000 (500MB)
|
||||||
|
|
@ -227,8 +224,7 @@ 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. None is returned if no images directory
|
|
||||||
was created
|
was created
|
||||||
"""
|
"""
|
||||||
im_path = os.path.join(self.dir_path, IMG_DIR_NAME)
|
im_path = os.path.join(self.dir_path, IMG_DIR_NAME)
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,7 @@ def enforce_xyz_tuple(value: XYZPos) -> XYZPos:
|
||||||
|
|
||||||
|
|
||||||
class ScanPlanner:
|
class ScanPlanner:
|
||||||
"""
|
"""A base class for a scan planner.
|
||||||
A base class for a scan planner.
|
|
||||||
|
|
||||||
This should never be used directly for a scan, it should be subclassed.
|
This should never be used directly for a scan, it should be subclassed.
|
||||||
|
|
||||||
|
|
@ -116,9 +115,7 @@ class ScanPlanner:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def imaged_locations(self) -> XYZPosList:
|
def imaged_locations(self) -> XYZPosList:
|
||||||
"""
|
"""Property to access a copy of the imaged_locations"""
|
||||||
Property to access a copy of the imaged_locations
|
|
||||||
"""
|
|
||||||
return copy(self._imaged_locations)
|
return copy(self._imaged_locations)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -148,22 +145,17 @@ class ScanPlanner:
|
||||||
raise NotImplementedError("Did you call the ScanPlanner base class?")
|
raise NotImplementedError("Did you call the ScanPlanner base class?")
|
||||||
|
|
||||||
def position_visited(self, position: XYPos) -> bool:
|
def position_visited(self, position: XYPos) -> bool:
|
||||||
"""
|
"""Return True if input xy position has been visited before"""
|
||||||
Return True if input xy position has been visited before
|
|
||||||
"""
|
|
||||||
# Ensure tuple for correct matching!
|
# Ensure tuple for correct matching!
|
||||||
return tuple(position) in self._path_history
|
return tuple(position) in self._path_history
|
||||||
|
|
||||||
def position_planned(self, position: XYPos) -> bool:
|
def position_planned(self, position: XYPos) -> bool:
|
||||||
"""
|
"""Return True if input xy position is planned"""
|
||||||
Return True if input xy position is planned
|
|
||||||
"""
|
|
||||||
# Ensure tuple for correct matching!
|
# Ensure tuple for correct matching!
|
||||||
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 the estimated z-position
|
|
||||||
for this location.
|
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
|
||||||
|
|
@ -184,8 +176,7 @@ 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
|
to the input xy_position, with the most recently taken image returned in
|
||||||
the case of a tie
|
the case of a tie
|
||||||
|
|
||||||
|
|
@ -212,8 +203,7 @@ class ScanPlanner:
|
||||||
def mark_location_visited(
|
def mark_location_visited(
|
||||||
self, xyz_pos: XYZPos, imaged: bool, focused: bool
|
self, xyz_pos: XYZPos, imaged: bool, focused: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Mark the location as visited
|
||||||
Mark the location as visited
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xyz_pos: the x_y_z position
|
xyz_pos: the x_y_z position
|
||||||
|
|
@ -290,8 +280,7 @@ class SmartSpiral(ScanPlanner):
|
||||||
def mark_location_visited(
|
def mark_location_visited(
|
||||||
self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True
|
self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Mark the location as visited. Adjust extra positions accordingly
|
||||||
Mark the location as visited. Adjust extra positions accordingly
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xyz_pos: the x_y_z position
|
xyz_pos: the x_y_z position
|
||||||
|
|
@ -336,9 +325,7 @@ class SmartSpiral(ScanPlanner):
|
||||||
self._remaining_locations.append(new_pos)
|
self._remaining_locations.append(new_pos)
|
||||||
|
|
||||||
def _re_sort_remaining_locations(self, current_pos: XYPos) -> None:
|
def _re_sort_remaining_locations(self, current_pos: XYPos) -> None:
|
||||||
"""
|
"""Sort the remaining positions besed on the current location"""
|
||||||
Sort the remaining positions besed on the current location
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Defined rather than use a lambda for readability
|
# Defined rather than use a lambda for readability
|
||||||
def sort_key(pos):
|
def sort_key(pos):
|
||||||
|
|
@ -351,8 +338,7 @@ 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 the estimated z-position
|
|
||||||
for this location. This overrides the default behaviour of ScanPlanner
|
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
|
to take the lowest value of nearest neighbours as this works best for smart stack
|
||||||
|
|
||||||
|
|
@ -374,8 +360,7 @@ class SmartSpiral(ScanPlanner):
|
||||||
return next_location, z
|
return next_location, z
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -412,8 +397,7 @@ 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 number of moves between two xy positions in the x or y direction
|
|
||||||
whichever is largest
|
whichever is largest
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -434,8 +418,7 @@ class SmartSpiral(ScanPlanner):
|
||||||
def distance_between(
|
def distance_between(
|
||||||
current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray
|
current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""Calculate the distance between the two xy positions
|
||||||
Calculate the distance between the two xy positions
|
|
||||||
|
|
||||||
This was previously called ``distance_to_site``
|
This was previously called ``distance_to_site``
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
|
||||||
|
|
||||||
|
|
||||||
def set_shutdown_function(shutdown_function: Callable[[], None]):
|
def set_shutdown_function(shutdown_function: Callable[[], None]):
|
||||||
"""
|
"""Ensure a function is called before the shutdown
|
||||||
Ensure a function is called before the shutdown
|
|
||||||
|
|
||||||
This monkey patches the Uvicorn Server's handle_exit. This is needed because
|
This monkey patches the Uvicorn Server's handle_exit. This is needed because
|
||||||
the uvicorn ``lifecycle`` events and FastAPI ``shutdown`` events only fire once
|
the uvicorn ``lifecycle`` events and FastAPI ``shutdown`` events only fire once
|
||||||
|
|
@ -52,8 +51,7 @@ def customise_server(
|
||||||
|
|
||||||
|
|
||||||
def _get_scans_dir(config: dict) -> Optional[str]:
|
def _get_scans_dir(config: dict) -> Optional[str]:
|
||||||
"""
|
"""Read the config and return the scans directory.
|
||||||
Read the config and return the scans directory.
|
|
||||||
|
|
||||||
Return is None if there is no /smart_scan/ thing loaded.
|
Return is None if there is no /smart_scan/ thing loaded.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,7 @@ class StackParams:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def steps_undershoot(self) -> int:
|
def steps_undershoot(self) -> int:
|
||||||
"""
|
"""The distance to deliberately undershoot the estimated optimal starting point"""
|
||||||
The distance to deliberately undershoot the estimated optimal starting point
|
|
||||||
"""
|
|
||||||
# Starting too low by "steps_undershoot" makes smart stacking faster.
|
# Starting too low by "steps_undershoot" makes smart stacking faster.
|
||||||
# Starting a stack too high requires it to move to the start,
|
# Starting a stack too high requires it to move to the start,
|
||||||
# autofocus and then re-stack. Starting slightly too low only
|
# autofocus and then re-stack. Starting slightly too low only
|
||||||
|
|
@ -135,9 +133,7 @@ class StackParams:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CaptureInfo:
|
class CaptureInfo:
|
||||||
"""
|
"""The information from a capture in a z_stack"""
|
||||||
The information from a capture in a z_stack
|
|
||||||
"""
|
|
||||||
|
|
||||||
buffer_id: int
|
buffer_id: int
|
||||||
position: dict[str, int]
|
position: dict[str, int]
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,7 @@ class NoImageInMemoryError(RuntimeError):
|
||||||
|
|
||||||
|
|
||||||
class CameraMemoryBuffer:
|
class CameraMemoryBuffer:
|
||||||
"""
|
"""A class that holds images in memory. The images are by default PIL images.
|
||||||
A class that holds images in memory. The images are by default PIL images.
|
|
||||||
|
|
||||||
However subclasses of BaseCamera can use this class to store other object types
|
However subclasses of BaseCamera can use this class to store other object types
|
||||||
"""
|
"""
|
||||||
|
|
@ -62,8 +61,7 @@ class CameraMemoryBuffer:
|
||||||
def add_image(
|
def add_image(
|
||||||
self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1
|
self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""Add an image to the Memory buffer
|
||||||
Add an image to the Memory buffer
|
|
||||||
|
|
||||||
This will add an image to the memory buffer. By default the buffer will
|
This will add an image to the memory buffer. By default the buffer will
|
||||||
be cleared. To allow saving multiple images the buffer_max must be set
|
be cleared. To allow saving multiple images the buffer_max must be set
|
||||||
|
|
@ -85,8 +83,7 @@ class CameraMemoryBuffer:
|
||||||
def get_image(
|
def get_image(
|
||||||
self, buffer_id: Optional[int] = None, remove: bool = True
|
self, buffer_id: Optional[int] = None, remove: bool = True
|
||||||
) -> tuple[Any, Optional[dict]]:
|
) -> tuple[Any, Optional[dict]]:
|
||||||
"""
|
"""Return the image with the given id.
|
||||||
Return the image with the given id.
|
|
||||||
|
|
||||||
If no id is given the most recent image is returned. However, the
|
If no id is given the most recent image is returned. However, the
|
||||||
buffer is also cleared, otherwise it would be possible to accidentally
|
buffer is also cleared, otherwise it would be possible to accidentally
|
||||||
|
|
@ -117,14 +114,11 @@ class CameraMemoryBuffer:
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""
|
"""Clear all images from memory"""
|
||||||
Clear all images from memory
|
|
||||||
"""
|
|
||||||
self._storage.clear()
|
self._storage.clear()
|
||||||
|
|
||||||
def _create_space(self, buffer_max: int) -> None:
|
def _create_space(self, buffer_max: int) -> None:
|
||||||
"""
|
"""Create space to add an image.
|
||||||
Create space to add an image.
|
|
||||||
|
|
||||||
:param buffer_max: The maximum number of images that should be in the buffer
|
:param buffer_max: The maximum number of images that should be in the buffer
|
||||||
once another images is added.
|
once another images is added.
|
||||||
|
|
@ -170,8 +164,7 @@ class BaseCamera(lt.Thing):
|
||||||
)
|
)
|
||||||
|
|
||||||
def kill_mjpeg_streams(self):
|
def kill_mjpeg_streams(self):
|
||||||
"""
|
"""Kill the streams now as the server is shutting down.
|
||||||
Kill the streams now as the server is shutting down.
|
|
||||||
|
|
||||||
This is called when uvicorn gets the a shutdown signal. As this is called from
|
This is called when uvicorn gets the a shutdown signal. As this is called from
|
||||||
the event loop it cannot interact with the our ThingProperties or run
|
the event loop it cannot interact with the our ThingProperties or run
|
||||||
|
|
@ -293,8 +286,7 @@ class BaseCamera(lt.Thing):
|
||||||
metadata_getter: lt.deps.GetThingStates,
|
metadata_getter: lt.deps.GetThingStates,
|
||||||
buffer_max: int = 1,
|
buffer_max: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Capture an image to memory. This can be saved later with ``save_from_memory``
|
||||||
Capture an image to memory. This can be saved later with ``save_from_memory``
|
|
||||||
|
|
||||||
Note that only one image is held in memory so this will overwrite any image
|
Note that only one image is held in memory so this will overwrite any image
|
||||||
in memory.
|
in memory.
|
||||||
|
|
@ -319,8 +311,7 @@ class BaseCamera(lt.Thing):
|
||||||
save_resolution: Optional[Tuple[int, int]] = None,
|
save_resolution: Optional[Tuple[int, int]] = None,
|
||||||
buffer_id: Optional[int] = None,
|
buffer_id: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Save an image that has been captured to memory.
|
||||||
Save an image that has been captured to memory.
|
|
||||||
|
|
||||||
:param jpeg_path: The path to save the file to
|
:param jpeg_path: The path to save the file to
|
||||||
:param logger: This should be injected automatically by Labthings FastAPI
|
:param logger: This should be injected automatically by Labthings FastAPI
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,7 @@ 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 as reported by the PiCamera.
|
sensor mode as reported by the PiCamera.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -77,8 +76,7 @@ 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. The output size and the bit depth.
|
Sensor mode. The output size and the bit depth.
|
||||||
|
|
||||||
This is a Pydantic modell so that it can be saved to the disk.
|
This is a Pydantic modell so that it can be saved to the disk.
|
||||||
|
|
@ -89,8 +87,7 @@ class SensorModeSelector(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class LensShading(BaseModel):
|
class LensShading(BaseModel):
|
||||||
"""
|
"""A Pydantic model holding the lens shading tables.
|
||||||
A Pydantic model holding the lens shading tables.
|
|
||||||
|
|
||||||
PiCamera needs three numpy arrays for lens shading correction. Each array is
|
PiCamera needs three numpy arrays for lens shading correction. Each array is
|
||||||
(12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and
|
(12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and
|
||||||
|
|
@ -363,8 +360,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
def start_streaming(
|
def start_streaming(
|
||||||
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Start the MJPEG stream. This is where persistent controls are sent to camera.
|
||||||
Start the MJPEG stream. This is where persistent controls are sent to camera.
|
|
||||||
|
|
||||||
Sets the camera resolutions based on input parameters, and sets the low-res
|
Sets the camera resolutions based on input parameters, and sets the low-res
|
||||||
resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera
|
resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera
|
||||||
|
|
@ -434,9 +430,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def stop_streaming(self, stop_web_stream: bool = True) -> None:
|
def stop_streaming(self, stop_web_stream: bool = True) -> None:
|
||||||
"""
|
"""Stop the MJPEG stream"""
|
||||||
Stop the MJPEG stream
|
|
||||||
"""
|
|
||||||
with self._streaming_picamera() as picam:
|
with self._streaming_picamera() as picam:
|
||||||
try:
|
try:
|
||||||
picam.stop_recording() # This should also stop the extra lores encoder
|
picam.stop_recording() # This should also stop the extra lores encoder
|
||||||
|
|
@ -679,8 +673,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def reset_ccm(self):
|
def reset_ccm(self):
|
||||||
"""
|
"""Overwrite the colour correction matrix in camera tuning with default values.
|
||||||
Overwrite the colour correction matrix in camera tuning with default values.
|
|
||||||
|
|
||||||
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page
|
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page
|
||||||
45.
|
45.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Functions to set up a Raspberry Pi Camera v2 for scientific use
|
||||||
Functions to set up a Raspberry Pi Camera v2 for scientific use
|
|
||||||
|
|
||||||
This module provides slower, simpler functions to set the
|
This module provides slower, simpler functions to set the
|
||||||
gain, exposure, and white balance of a Raspberry Pi camera, using
|
gain, exposure, and white balance of a Raspberry Pi camera, using
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""OpenFlexure Microscope API extension for stage calibration
|
||||||
OpenFlexure Microscope API extension for stage calibration
|
|
||||||
|
|
||||||
This file contains the HTTP API for camera/stage calibration. It
|
This file contains the HTTP API for camera/stage calibration. It
|
||||||
includes calibration functions that measure the relationship between
|
includes calibration functions that measure the relationship between
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""OpenFlexure Settings Management
|
||||||
OpenFlexure Settings Management
|
|
||||||
|
|
||||||
This module provides some settings management across the other Things, and
|
This module provides some settings management across the other Things, and
|
||||||
for code that currently lives in clients but needs to persist settings on
|
for code that currently lives in clients but needs to persist settings on
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]:
|
def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]:
|
||||||
"""
|
"""Take a test image and use camera stage mapping to calculate x and y displacement
|
||||||
Take a test image and use camera stage mapping to calculate x and y displacement
|
|
||||||
|
|
||||||
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
|
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
|
||||||
that each image should overlap its nearest neighbour by 50%.
|
that each image should overlap its nearest neighbour by 50%.
|
||||||
|
|
@ -312,8 +311,7 @@ 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, to allow
|
|
||||||
the user to review the settings used in the scan
|
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?
|
||||||
|
|
@ -337,8 +335,7 @@ 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 as a JSON file in the scan folder, 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",
|
||||||
|
|
@ -372,9 +369,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _manage_stitching_threads(self):
|
def _manage_stitching_threads(self):
|
||||||
"""
|
"""Manage the stitching threads, starting them if needed and not already running."""
|
||||||
Manage the stitching threads, starting them if needed and not already running.
|
|
||||||
"""
|
|
||||||
# Assume 4 images means at least one offset in x and y, making the stitching
|
# Assume 4 images means at least one offset in x and y, making the stitching
|
||||||
# well constrained.
|
# well constrained.
|
||||||
if self._scan_images_taken > 3:
|
if self._scan_images_taken > 3:
|
||||||
|
|
@ -383,8 +378,7 @@ 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, handling the threads performing
|
|
||||||
stitching. Uses the result (or exception) from the scanning to
|
stitching. Uses the result (or exception) from the scanning to
|
||||||
determine whether the scan should be stitched and the microscope
|
determine whether the scan should be stitched and the microscope
|
||||||
should return to the starting x,y,z position
|
should return to the starting x,y,z position
|
||||||
|
|
@ -703,9 +697,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None:
|
def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None:
|
||||||
"""
|
"""Delete all scan folders containing no images at the top level"""
|
||||||
Delete all scan folders containing no images at the top level
|
|
||||||
"""
|
|
||||||
# JSON is ignored as it's created before any images are captured
|
# JSON is ignored as it's created before any images are captured
|
||||||
for scan_info in self._scan_dir_manager.all_scans_info():
|
for scan_info in self._scan_dir_manager.all_scans_info():
|
||||||
if scan_info.number_of_images == 0:
|
if scan_info.number_of_images == 0:
|
||||||
|
|
@ -813,9 +805,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _preview_stitch_wait(self):
|
def _preview_stitch_wait(self):
|
||||||
"""
|
"""Wait for an ongoing preview stitch to return"""
|
||||||
Wait for an ongoing preview stitch to return
|
|
||||||
"""
|
|
||||||
if self._preview_stitch_running():
|
if self._preview_stitch_running():
|
||||||
with self._preview_stitch_popen_lock:
|
with self._preview_stitch_popen_lock:
|
||||||
self._preview_stitch_popen.wait()
|
self._preview_stitch_popen.wait()
|
||||||
|
|
@ -826,8 +816,7 @@ class SmartScanThing(lt.Thing):
|
||||||
cancel: lt.deps.CancelHook,
|
cancel: lt.deps.CancelHook,
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
) -> CompletedProcess:
|
) -> CompletedProcess:
|
||||||
"""
|
"""Run a subprocess and log any output
|
||||||
Run a subprocess and log any output
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ChildProcessError if exit code is not zero
|
ChildProcessError if exit code is not zero
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""OpenFlexure Microscope system control Thing
|
||||||
OpenFlexure Microscope system control Thing
|
|
||||||
|
|
||||||
This module defines a Thing that can shut down or restart the host computer.
|
This module defines a Thing that can shut down or restart the host computer.
|
||||||
"""
|
"""
|
||||||
|
|
@ -16,15 +15,11 @@ class CommandOutput(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class SystemControlThing(lt.Thing):
|
class SystemControlThing(lt.Thing):
|
||||||
"""
|
"""Attempt to shutdown the device"""
|
||||||
Attempt to shutdown the device
|
|
||||||
"""
|
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def shutdown(self) -> CommandOutput:
|
def shutdown(self) -> CommandOutput:
|
||||||
"""
|
"""Attempt to shutdown the device"""
|
||||||
Attempt to shutdown the device
|
|
||||||
"""
|
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
["sudo", "shutdown", "-h", "now"],
|
["sudo", "shutdown", "-h", "now"],
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@ class ErrorCapturingThread(Thread):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None):
|
def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None):
|
||||||
"""
|
"""Initialise with the same arguments as Thread"""
|
||||||
Initialise with the same arguments as Thread
|
|
||||||
"""
|
|
||||||
# As all inputs are keywords we need to set the default values for args and kwargs:
|
# As all inputs are keywords we need to set the default values for args and kwargs:
|
||||||
if args is None:
|
if args is None:
|
||||||
args = ()
|
args = ()
|
||||||
|
|
@ -45,8 +43,7 @@ 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. If the thread ended due to an unhandled exception,
|
|
||||||
the exception will be raised when this method is called.
|
the exception will be raised when this method is called.
|
||||||
"""
|
"""
|
||||||
super().join(timeout)
|
super().join(timeout)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
"""
|
"""Tests for the built in buffer for the camera."""
|
||||||
Tests for the built in buffer for the camera.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,9 +156,7 @@ def test_basic_directory_operations():
|
||||||
|
|
||||||
|
|
||||||
def test_scan_sequence_and_listing():
|
def test_scan_sequence_and_listing():
|
||||||
"""
|
"""Check created scans are added in order and listed correctly"""
|
||||||
Check created scans are added in order and listed correctly
|
|
||||||
"""
|
|
||||||
_clear_scan_dir()
|
_clear_scan_dir()
|
||||||
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
||||||
# Make 4 scans
|
# Make 4 scans
|
||||||
|
|
@ -185,8 +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 created scans is the correct name if the directories
|
|
||||||
are not sequential
|
are not sequential
|
||||||
"""
|
"""
|
||||||
_clear_scan_dir()
|
_clear_scan_dir()
|
||||||
|
|
@ -213,9 +210,7 @@ def test_scan_name_non_sequential():
|
||||||
|
|
||||||
|
|
||||||
def test_all_scan_names_taken():
|
def test_all_scan_names_taken():
|
||||||
"""
|
"""If the next sequential scan name needs more than 4 digits check error is thrown."""
|
||||||
If the next sequential scan name needs more than 4 digits check error is thrown.
|
|
||||||
"""
|
|
||||||
_clear_scan_dir()
|
_clear_scan_dir()
|
||||||
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_9999"))
|
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_9999"))
|
||||||
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
||||||
|
|
@ -224,9 +219,7 @@ def test_all_scan_names_taken():
|
||||||
|
|
||||||
|
|
||||||
def test_no_scan_names_given():
|
def test_no_scan_names_given():
|
||||||
"""
|
"""Check correct default scan name is used if empty string is given"""
|
||||||
Check correct default scan name is used if empty string is given
|
|
||||||
"""
|
|
||||||
_clear_scan_dir()
|
_clear_scan_dir()
|
||||||
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
||||||
scan_dir = scan_dir_manager.new_scan_dir("")
|
scan_dir = scan_dir_manager.new_scan_dir("")
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,7 @@ 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
|
||||||
The number of steps gets very large in reality runs some tests to check
|
|
||||||
that everything works well with huge numbers of steps
|
that everything works well with huge numbers of steps
|
||||||
"""
|
"""
|
||||||
intial_position = (0, 0)
|
intial_position = (0, 0)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Test the SmartScanThing *without* connecting it to a LabThings Server.
|
||||||
Test the SmartScanThing *without* connecting it to a LabThings Server.
|
|
||||||
|
|
||||||
By testing without connecting to the LabThings server it is possible to
|
By testing without connecting to the LabThings server it is possible to
|
||||||
directly poll any properties and to start any methods.
|
directly poll any properties and to start any methods.
|
||||||
|
|
@ -151,8 +150,7 @@ def test_delete_all_scans(smart_scan_thing, caplog):
|
||||||
|
|
||||||
|
|
||||||
def _run_only_outer_scan(adjust_inital_state: Optional[Callable] = None):
|
def _run_only_outer_scan(adjust_inital_state: Optional[Callable] = None):
|
||||||
"""
|
"""Create a subclass of SmartScanThing to mock _run_scan and run sample_scan
|
||||||
Create a subclass of SmartScanThing to mock _run_scan and run sample_scan
|
|
||||||
|
|
||||||
This should do all the set up for a scan, move into the mocked
|
This should do all the set up for a scan, move into the mocked
|
||||||
_run_scan method where this can be tested. Once this is done
|
_run_scan method where this can be tested. Once this is done
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Tests for the smart/fast stacking.
|
||||||
Tests for the smart/fast stacking.
|
|
||||||
|
|
||||||
Currently these tests don't test the Thing itself, just surrounding functionality
|
Currently these tests don't test the Thing itself, just surrounding functionality
|
||||||
"""
|
"""
|
||||||
|
|
@ -146,8 +145,7 @@ def test_even_images_to_save(save_ims, extra_ims):
|
||||||
|
|
||||||
|
|
||||||
def test_computed_stack_params():
|
def test_computed_stack_params():
|
||||||
"""
|
"""Test StackParams computed properties are as expected
|
||||||
Test StackParams computed properties are as expected
|
|
||||||
|
|
||||||
Not using hypothesis or we will just copy in the same formulas.
|
Not using hypothesis or we will just copy in the same formulas.
|
||||||
"""
|
"""
|
||||||
|
|
@ -188,8 +186,7 @@ def test_computed_stack_params():
|
||||||
|
|
||||||
|
|
||||||
def random_capture(set_id: Optional[int] = None):
|
def random_capture(set_id: Optional[int] = None):
|
||||||
"""
|
"""Create a capture with random values
|
||||||
Create a capture with random values
|
|
||||||
|
|
||||||
:param set_id: Optional, use to set a fixed id rather than a random one
|
:param set_id: Optional, use to set a fixed id rather than a random one
|
||||||
"""
|
"""
|
||||||
|
|
@ -206,18 +203,14 @@ def random_capture(set_id: Optional[int] = None):
|
||||||
|
|
||||||
|
|
||||||
def test_capture_filename_matches_regex():
|
def test_capture_filename_matches_regex():
|
||||||
"""
|
"""For 100 random captures check the image always matches the regex"""
|
||||||
For 100 random captures check the image always matches the regex
|
|
||||||
"""
|
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
assert IMAGE_REGEX.search(random_capture().filename)
|
assert IMAGE_REGEX.search(random_capture().filename)
|
||||||
|
|
||||||
|
|
||||||
@given(st.integers(min_value=0, max_value=5000))
|
@given(st.integers(min_value=0, max_value=5000))
|
||||||
def test_retrieval_of_captures(start):
|
def test_retrieval_of_captures(start):
|
||||||
"""
|
"""For 20 random captures, check each can be retrieved correctly by id"""
|
||||||
For 20 random captures, check each can be retrieved correctly by id
|
|
||||||
"""
|
|
||||||
captures = [random_capture(start + i) for i in range(20)]
|
captures = [random_capture(start + i) for i in range(20)]
|
||||||
|
|
||||||
for i, capture in enumerate(captures):
|
for i, capture in enumerate(captures):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Utilities for testing and debugging.
|
||||||
Utilities for testing and debugging.
|
|
||||||
|
|
||||||
At the top level are some basic testing functions. More specific testing utilities are
|
At the top level are some basic testing functions. More specific testing utilities are
|
||||||
provided by modules inside the package.
|
provided by modules inside the package.
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,18 @@ 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. The sample is able to return
|
|
||||||
whether a given position is sample, no image associated with the sample
|
whether a given position is sample, 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 whether an image at a given location with a given image size
|
|
||||||
is on the sample
|
is on the sample
|
||||||
|
|
||||||
This doesn't check the entire image field as this is designed to be used
|
This doesn't check the entire image field as this is designed to be used
|
||||||
|
|
@ -47,18 +44,14 @@ class FakeSample:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patch(self) -> PathPatch:
|
def patch(self) -> PathPatch:
|
||||||
"""
|
"""The sample as a matplotlib patch for plotting"""
|
||||||
The sample as a matplotlib patch for plotting
|
|
||||||
"""
|
|
||||||
patch = PathPatch(self._sample_perimeter)
|
patch = PathPatch(self._sample_perimeter)
|
||||||
patch.set(color=(1.0, 0.8, 1.0, 1.0))
|
patch.set(color=(1.0, 0.8, 1.0, 1.0))
|
||||||
return patch
|
return patch
|
||||||
|
|
||||||
|
|
||||||
def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Figure:
|
def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Figure:
|
||||||
"""
|
"""For a given sample and scanner object return a matplotlib figure of the scan"""
|
||||||
For a given sample and scanner object return a matplotlib figure of the scan
|
|
||||||
"""
|
|
||||||
fig, ax = plt.subplots(figsize=(8, 8))
|
fig, ax = plt.subplots(figsize=(8, 8))
|
||||||
ax.add_artist(sample.patch)
|
ax.add_artist(sample.patch)
|
||||||
xh, yh = zip(*planner._path_history)
|
xh, yh = zip(*planner._path_history)
|
||||||
|
|
@ -84,8 +77,7 @@ 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
|
||||||
Given a lists of xy_points interpolate an n_point closed curve. This can be used
|
|
||||||
to create an arbitrary sample shape plan a scan.
|
to create an arbitrary sample shape plan a scan.
|
||||||
|
|
||||||
Modified from:
|
Modified from:
|
||||||
|
|
@ -113,8 +105,7 @@ 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 and return the sample scanned and the planner object
|
|
||||||
after scan is complete
|
after scan is complete
|
||||||
"""
|
"""
|
||||||
xy_sample_points = load_sample_points(sample_name)
|
xy_sample_points = load_sample_points(sample_name)
|
||||||
|
|
@ -135,8 +126,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -162,8 +152,7 @@ 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.
|
||||||
|
|
||||||
|
|
@ -182,8 +171,7 @@ 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 pickled, so that it can be committed.
|
||||||
"""
|
"""
|
||||||
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")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue