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
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ def test_calibration(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.
|
||||
"""
|
||||
# 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
|
||||
to monitor progress.
|
||||
|
|
@ -19,8 +18,7 @@ logging.basicConfig(level=logging.DEBUG)
|
|||
|
||||
|
||||
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
|
||||
"""
|
||||
|
|
@ -64,8 +62,6 @@ def _test_exposure_time_drift(desired_time):
|
|||
|
||||
|
||||
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]:
|
||||
_test_exposure_time_drift(desired_time)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ logging.basicConfig(level=logging.DEBUG)
|
|||
|
||||
|
||||
def test_sensor_mode():
|
||||
"""
|
||||
Test capturing raw arrays in two different sensor modes
|
||||
"""
|
||||
"""Test capturing raw arrays in two different sensor modes"""
|
||||
cam = StreamingPiCamera2()
|
||||
server = ThingServer()
|
||||
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
|
||||
|
||||
|
|
@ -16,17 +14,13 @@ MODEL = Picamera2.global_camera_info()[0]["Model"]
|
|||
|
||||
|
||||
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"
|
||||
return Picamera2.load_tuning_file(fname)
|
||||
|
||||
|
||||
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()
|
||||
bad_tuning = default_tuning.copy()
|
||||
bad_tuning["version"] = 999
|
||||
|
|
@ -34,8 +28,7 @@ def generate_bad_tuning():
|
|||
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -53,8 +46,7 @@ def print_tuning(read_file: 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.
|
||||
|
||||
: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:
|
||||
"""
|
||||
Start the server in a subprocess.
|
||||
"""Start the server in a subprocess.
|
||||
|
||||
The server is started in a subprocess and all outputs are buffered.
|
||||
|
||||
|
|
|
|||
|
|
@ -128,8 +128,7 @@ ignore = [
|
|||
"D203", # incompatible with D204
|
||||
"D213", # incompatible with D212
|
||||
# Below are checkers to be turned on gradually
|
||||
"D212",
|
||||
"D205",
|
||||
"D205", # Force single line summary!
|
||||
"D415",
|
||||
"D400",
|
||||
"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/
|
||||
Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved.
|
||||
SPDX-License-Identifier: MIT-DLR
|
||||
|
|
|
|||
|
|
@ -68,8 +68,7 @@ def retrieve_log_from_file() -> PlainTextResponse:
|
|||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
|
@ -79,8 +78,7 @@ class OFMHandler(logging.Handler):
|
|||
self._max_logs = max_logs
|
||||
|
||||
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
|
||||
"""
|
||||
self._log.append(self.format(record))
|
||||
|
|
@ -88,9 +86,7 @@ class OFMHandler(logging.Handler):
|
|||
self._log.pop(0)
|
||||
|
||||
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
|
||||
# These are the logs each time an API endpoint is accessed
|
||||
# This is only the log for the UI.
|
||||
|
|
@ -108,9 +104,7 @@ class OFMHandler(logging.Handler):
|
|||
|
||||
@property
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,7 @@ class ScanInfo(BaseModel):
|
|||
|
||||
|
||||
class ScanDirectoryManager:
|
||||
"""
|
||||
A class for managing interactions with scan directories
|
||||
"""
|
||||
"""A class for managing interactions with scan directories"""
|
||||
|
||||
_base_scan_dir: str
|
||||
|
||||
|
|
@ -180,8 +178,7 @@ class ScanDirectoryManager:
|
|||
return ScanDirectory(scan_name, self.base_dir).zip_files(final_version)
|
||||
|
||||
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.
|
||||
Default = 500,000,000 (500MB)
|
||||
|
|
@ -227,8 +224,7 @@ class ScanDirectory:
|
|||
|
||||
@property
|
||||
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
|
||||
"""
|
||||
im_path = os.path.join(self.dir_path, IMG_DIR_NAME)
|
||||
|
|
|
|||
|
|
@ -58,8 +58,7 @@ def enforce_xyz_tuple(value: XYZPos) -> XYZPos:
|
|||
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -116,9 +115,7 @@ class ScanPlanner:
|
|||
|
||||
@property
|
||||
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)
|
||||
|
||||
@property
|
||||
|
|
@ -148,22 +145,17 @@ class ScanPlanner:
|
|||
raise NotImplementedError("Did you call the ScanPlanner base class?")
|
||||
|
||||
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!
|
||||
return tuple(position) in self._path_history
|
||||
|
||||
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!
|
||||
return tuple(position) in self._remaining_locations
|
||||
|
||||
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.
|
||||
|
||||
Note z-position may be None! This indicates that the current z, position
|
||||
|
|
@ -184,8 +176,7 @@ class ScanPlanner:
|
|||
return next_location, z
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -212,8 +203,7 @@ class ScanPlanner:
|
|||
def mark_location_visited(
|
||||
self, xyz_pos: XYZPos, imaged: bool, focused: bool
|
||||
) -> None:
|
||||
"""
|
||||
Mark the location as visited
|
||||
"""Mark the location as visited
|
||||
|
||||
Args:
|
||||
xyz_pos: the x_y_z position
|
||||
|
|
@ -290,8 +280,7 @@ class SmartSpiral(ScanPlanner):
|
|||
def mark_location_visited(
|
||||
self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Mark the location as visited. Adjust extra positions accordingly
|
||||
"""Mark the location as visited. Adjust extra positions accordingly
|
||||
|
||||
Args:
|
||||
xyz_pos: the x_y_z position
|
||||
|
|
@ -336,9 +325,7 @@ class SmartSpiral(ScanPlanner):
|
|||
self._remaining_locations.append(new_pos)
|
||||
|
||||
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
|
||||
def sort_key(pos):
|
||||
|
|
@ -351,8 +338,7 @@ class SmartSpiral(ScanPlanner):
|
|||
self._remaining_locations.sort(key=sort_key)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
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.
|
||||
|
|
@ -412,8 +397,7 @@ class SmartSpiral(ScanPlanner):
|
|||
starting_pos: XYPos | np.ndarray,
|
||||
ending_pos: XYPos | np.ndarray,
|
||||
) -> 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
|
||||
|
||||
Args:
|
||||
|
|
@ -434,8 +418,7 @@ class SmartSpiral(ScanPlanner):
|
|||
def distance_between(
|
||||
current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the distance between the two xy positions
|
||||
"""Calculate the distance between the two xy positions
|
||||
|
||||
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]):
|
||||
"""
|
||||
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
|
||||
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]:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -107,9 +107,7 @@ class StackParams:
|
|||
|
||||
@property
|
||||
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 a stack too high requires it to move to the start,
|
||||
# autofocus and then re-stack. Starting slightly too low only
|
||||
|
|
@ -135,9 +133,7 @@ class StackParams:
|
|||
|
||||
@dataclass
|
||||
class CaptureInfo:
|
||||
"""
|
||||
The information from a capture in a z_stack
|
||||
"""
|
||||
"""The information from a capture in a z_stack"""
|
||||
|
||||
buffer_id: int
|
||||
position: dict[str, int]
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ class NoImageInMemoryError(RuntimeError):
|
|||
|
||||
|
||||
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
|
||||
"""
|
||||
|
|
@ -62,8 +61,7 @@ class CameraMemoryBuffer:
|
|||
def add_image(
|
||||
self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1
|
||||
) -> 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
|
||||
be cleared. To allow saving multiple images the buffer_max must be set
|
||||
|
|
@ -85,8 +83,7 @@ class CameraMemoryBuffer:
|
|||
def get_image(
|
||||
self, buffer_id: Optional[int] = None, remove: bool = True
|
||||
) -> 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
|
||||
buffer is also cleared, otherwise it would be possible to accidentally
|
||||
|
|
@ -117,14 +114,11 @@ class CameraMemoryBuffer:
|
|||
) from e
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear all images from memory
|
||||
"""
|
||||
"""Clear all images from memory"""
|
||||
self._storage.clear()
|
||||
|
||||
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
|
||||
once another images is added.
|
||||
|
|
@ -170,8 +164,7 @@ class BaseCamera(lt.Thing):
|
|||
)
|
||||
|
||||
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
|
||||
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,
|
||||
buffer_max: int = 1,
|
||||
) -> 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
|
||||
in memory.
|
||||
|
|
@ -319,8 +311,7 @@ class BaseCamera(lt.Thing):
|
|||
save_resolution: Optional[Tuple[int, int]] = None,
|
||||
buffer_id: Optional[int] = 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 logger: This should be injected automatically by Labthings FastAPI
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ class PicameraStreamOutput(Output):
|
|||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
|
@ -77,8 +76,7 @@ class SensorMode(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.
|
||||
|
||||
This is a Pydantic modell so that it can be saved to the disk.
|
||||
|
|
@ -89,8 +87,7 @@ class SensorModeSelector(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
|
||||
(12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and
|
||||
|
|
@ -363,8 +360,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
def start_streaming(
|
||||
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
||||
) -> 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
|
||||
resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera
|
||||
|
|
@ -434,9 +430,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
def stop_streaming(self, stop_web_stream: bool = True) -> None:
|
||||
"""
|
||||
Stop the MJPEG stream
|
||||
"""
|
||||
"""Stop the MJPEG stream"""
|
||||
with self._streaming_picamera() as picam:
|
||||
try:
|
||||
picam.stop_recording() # This should also stop the extra lores encoder
|
||||
|
|
@ -679,8 +673,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
for code that currently lives in clients but needs to persist settings on
|
||||
|
|
|
|||
|
|
@ -229,8 +229,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
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
|
||||
that each image should overlap its nearest neighbour by 50%.
|
||||
|
|
@ -312,8 +311,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
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
|
||||
"""
|
||||
# Should this be a method of the scan_data dataclass?
|
||||
|
|
@ -337,8 +335,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
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.
|
||||
|
||||
Takes scan_result, a string that is either "success",
|
||||
|
|
@ -372,9 +369,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
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
|
||||
# well constrained.
|
||||
if self._scan_images_taken > 3:
|
||||
|
|
@ -383,8 +378,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
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
|
||||
determine whether the scan should be stitched and the microscope
|
||||
should return to the starting x,y,z position
|
||||
|
|
@ -703,9 +697,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
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
|
||||
for scan_info in self._scan_dir_manager.all_scans_info():
|
||||
if scan_info.number_of_images == 0:
|
||||
|
|
@ -813,9 +805,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
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():
|
||||
with self._preview_stitch_popen_lock:
|
||||
self._preview_stitch_popen.wait()
|
||||
|
|
@ -826,8 +816,7 @@ class SmartScanThing(lt.Thing):
|
|||
cancel: lt.deps.CancelHook,
|
||||
cmd: list[str],
|
||||
) -> CompletedProcess:
|
||||
"""
|
||||
Run a subprocess and log any output
|
||||
"""Run a subprocess and log any output
|
||||
|
||||
Raises:
|
||||
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.
|
||||
"""
|
||||
|
|
@ -16,15 +15,11 @@ class CommandOutput(BaseModel):
|
|||
|
||||
|
||||
class SystemControlThing(lt.Thing):
|
||||
"""
|
||||
Attempt to shutdown the device
|
||||
"""
|
||||
"""Attempt to shutdown the device"""
|
||||
|
||||
@lt.thing_action
|
||||
def shutdown(self) -> CommandOutput:
|
||||
"""
|
||||
Attempt to shutdown the device
|
||||
"""
|
||||
"""Attempt to shutdown the device"""
|
||||
p = subprocess.Popen(
|
||||
["sudo", "shutdown", "-h", "now"],
|
||||
stderr=subprocess.PIPE,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ class ErrorCapturingThread(Thread):
|
|||
"""
|
||||
|
||||
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:
|
||||
if args is None:
|
||||
args = ()
|
||||
|
|
@ -45,8 +43,7 @@ class ErrorCapturingThread(Thread):
|
|||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -156,9 +156,7 @@ def test_basic_directory_operations():
|
|||
|
||||
|
||||
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()
|
||||
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
||||
# Make 4 scans
|
||||
|
|
@ -185,8 +183,7 @@ def test_scan_sequence_and_listing():
|
|||
|
||||
|
||||
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
|
||||
"""
|
||||
_clear_scan_dir()
|
||||
|
|
@ -213,9 +210,7 @@ def test_scan_name_non_sequential():
|
|||
|
||||
|
||||
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()
|
||||
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_9999"))
|
||||
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
|
||||
|
|
@ -224,9 +219,7 @@ def test_all_scan_names_taken():
|
|||
|
||||
|
||||
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()
|
||||
scan_dir_manager = ScanDirectoryManager(BASE_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():
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
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):
|
||||
"""
|
||||
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
|
||||
_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
|
||||
"""
|
||||
|
|
@ -146,8 +145,7 @@ def test_even_images_to_save(save_ims, extra_ims):
|
|||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
|
@ -188,8 +186,7 @@ def test_computed_stack_params():
|
|||
|
||||
|
||||
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
|
||||
"""
|
||||
|
|
@ -206,18 +203,14 @@ def random_capture(set_id: Optional[int] = None):
|
|||
|
||||
|
||||
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):
|
||||
assert IMAGE_REGEX.search(random_capture().filename)
|
||||
|
||||
|
||||
@given(st.integers(min_value=0, max_value=5000))
|
||||
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)]
|
||||
|
||||
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
|
||||
provided by modules inside the package.
|
||||
|
|
|
|||
|
|
@ -14,21 +14,18 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
self._sample_perimeter = interp_closed_path(xy_points, 500)
|
||||
|
||||
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
|
||||
|
||||
This doesn't check the entire image field as this is designed to be used
|
||||
|
|
@ -47,18 +44,14 @@ class FakeSample:
|
|||
|
||||
@property
|
||||
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.set(color=(1.0, 0.8, 1.0, 1.0))
|
||||
return patch
|
||||
|
||||
|
||||
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))
|
||||
ax.add_artist(sample.patch)
|
||||
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:
|
||||
"""
|
||||
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.
|
||||
|
||||
Modified from:
|
||||
|
|
@ -113,8 +105,7 @@ def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPa
|
|||
def example_smart_spiral(
|
||||
sample_name: str = "lobed",
|
||||
) -> 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
|
||||
"""
|
||||
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():
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -162,8 +152,7 @@ def profile_and_save_plot_for_example_smart_spiral():
|
|||
|
||||
|
||||
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
|
||||
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(
|
||||
sample_name: str,
|
||||
) -> 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.
|
||||
"""
|
||||
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