Add punctuation to docstrings

This commit is contained in:
Julian Stirling 2025-07-10 02:03:02 +01:00
parent 4dc41bb008
commit 80beeea07b
34 changed files with 232 additions and 235 deletions

View file

@ -87,7 +87,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.
@ -105,7 +105,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)

View file

@ -20,7 +20,7 @@ class NotEnoughFreeSpaceError(IOError):
class ScanInfo(BaseModel):
"""Summary information about a scan folder"""
"""Summary information about a scan folder."""
name: str
created: float
@ -31,7 +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
@ -42,22 +42,22 @@ class ScanDirectoryManager:
@property
def base_dir(self) -> str:
"""The base directory scans are saved to"""
"""The base directory scans are saved to."""
return self._base_scan_dir
def exists(self, scan_name: str) -> bool:
"""Return True if scan of this name exists on disk"""
"""Return True if scan of this name exists on disk."""
return os.path.isdir(self.path_for(scan_name))
def path_for(self, scan_name: str) -> str:
"""Return the path for a given scan name
"""Return the path for a given scan name.
Returns the path even if it doesn't exist
"""
return os.path.join(self._base_scan_dir, scan_name)
def img_dir_for(self, scan_name: str) -> str:
"""Return the path for the image dir for a given scan name
"""Return the path for the image dir for a given scan name.
Returns the path even if it doesn't exist
"""
@ -66,7 +66,7 @@ class ScanDirectoryManager:
def get_file_path_from(
self, scan_name: str, filename: str, check_exists: bool = False
) -> Optional[str]:
"""Return the full file path for the file within a scan directory
"""Return the full file path for the file within a scan directory.
If check_exists is True then None will be returned if the file does
not exist.
@ -80,7 +80,7 @@ class ScanDirectoryManager:
def get_file_path_from_img_dir(
self, scan_name: str, filename: str, check_exists: bool = False
) -> Optional[str]:
"""Return the full file path for the file within a scan directory
"""Return the full file path for the file within a scan directory.
If check_exists is True, None is returned if the file does not exist. If False
then the path is returned anyway
@ -103,18 +103,18 @@ class ScanDirectoryManager:
@property
def all_scans(self) -> list[str]:
"""Return a list of the scan names in the base directory"""
"""Return a list of the scan names in the base directory."""
return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()]
def all_scans_info(self) -> list[ScanInfo]:
"""Return a lists of ScanInfo objects for each scan"""
"""Return a lists of ScanInfo objects for each scan."""
all_info: list[ScanInfo] = []
for scan_name in self.all_scans:
all_info.append(ScanDirectory(scan_name, self.base_dir).scan_info())
return all_info
def _unique_scan_name(self, scan_name: str) -> str:
"""Get the next unique scan name starting with the given name
"""Get the next unique scan name starting with the given name.
For more explanation on the scan naming see `new_scan_dir`
"""
@ -143,7 +143,7 @@ class ScanDirectoryManager:
return f"{scan_name}_{scan_num:0{SCAN_ZERO_PAD_DIGITS}d}"
def new_scan_dir(self, scan_name: str) -> "ScanDirectory":
"""Get a unique name for this scan and create a directory for it
"""Get a unique name for this scan and create a directory for it.
The scan will be named ``{scan_name}_0001`` where the number is
zero-padded to be SCAN_ZERO_PAD_DIGITS digits long (to allow correct sorting if the
@ -165,11 +165,11 @@ class ScanDirectoryManager:
return ScanDirectory(full_scan_name, self.base_dir)
def delete_scan(self, scan_name: str) -> None:
"""Delete a scan"""
"""Delete a scan."""
shutil.rmtree(self.path_for(scan_name))
def zip_scan(self, scan_name: str, final_version: bool = False) -> "ScanDirectory":
"""Zips any images from the scan not yet zipped, return full path to zip
"""Zips any images from the scan not yet zipped, return full path to zip.
``final_version`` Set true to stitch all files not just the scan images
this should only be done at the end as it is not possible to update a file
@ -178,7 +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)
@ -194,7 +194,7 @@ class ScanDirectoryManager:
class ScanDirectory:
"""A class for handling interactions with scan directories
"""A class for handling interactions with scan directories.
Initalisation parameters:
name: the name of the scan (the scan directory basename)
@ -214,12 +214,12 @@ class ScanDirectory:
@property
def name(self) -> str:
"""The name of the scan"""
"""The name of the scan."""
return self._name
@property
def dir_path(self) -> str:
"""The full path to the scan directory"""
"""The full path to the scan directory."""
return os.path.join(self._base_scan_dir, self._name)
@property
@ -235,17 +235,17 @@ class ScanDirectory:
@property
def created_time(self) -> float:
"""The time the directory was created on disk"""
"""The time the directory was created on disk."""
return os.path.getctime(self.dir_path)
def get_scan_files(self):
"""Return a list of the files in the images dir"""
"""Return a list of the files in the images dir."""
if self.images_dir is None:
return []
return os.listdir(self.images_dir)
def _extract_scan_images(self, file_list: list[str]):
"""Extract files which match the naming convention for scan images
"""Extract files which match the naming convention for scan images.
:param file_list: The list of files to search. Normally this would be
``self.get_scan_files()``
@ -255,7 +255,7 @@ class ScanDirectory:
return [i for i in file_list if IMAGE_REGEX.search(i)]
def _extract_final_stitches(self, file_list: list[str]):
"""Extract files which match the naming convention for final stitches
"""Extract files which match the naming convention for final stitches.
:param file_list: The list of files to search.
@ -264,7 +264,7 @@ class ScanDirectory:
return [i for i in file_list if STITCH_REGEX.search(i)]
def _extract_dzi_files(self, file_list: list[str]):
"""Extract files which match the naming convention for dzi_files
"""Extract files which match the naming convention for dzi_files.
:param file_list: The list of files to search.
@ -273,7 +273,7 @@ class ScanDirectory:
return [i for i in file_list if i.endswith("dzi")]
def get_final_stitch_name(self) -> Optional[str]:
"""Return the filename for the final stitch (in the images dir)
"""Return the filename for the final stitch (in the images dir).
If no final stitch is found, return None
"""
@ -283,11 +283,11 @@ class ScanDirectory:
return stitches[0]
def get_modified_time(self) -> float:
"""Return the modified time of the directory"""
"""Return the modified time of the directory."""
return max(os.stat(root).st_mtime for root, _, _ in os.walk(self.dir_path))
def scan_info(self) -> ScanInfo:
"""Return the information for the scan directory as a ScanInfo object"""
"""Return the information for the scan directory as a ScanInfo object."""
scan_files = self.get_scan_files()
scan_images = self._extract_scan_images(scan_files)
stitches = self._extract_final_stitches(scan_files)
@ -325,7 +325,7 @@ class ScanDirectory:
return files
def zip_files(self, final_version: bool = False) -> str:
"""Zips any images from the scan not yet zipped, return full path to zip
"""Zips any images from the scan not yet zipped, return full path to zip.
``final_version`` Set true to stitch all files not just the scan images
this should only be done at the end as it is not possible to update a file
@ -357,6 +357,6 @@ class ScanDirectory:
def get_files_in_zip(zip_path):
"""List the relative paths of all files and folders in the zip folder specified"""
"""List the relative paths of all files and folders in the zip folder specified."""
scan_zip = zipfile.ZipFile(zip_path)
return [os.path.normpath(i) for i in scan_zip.namelist()]

View file

@ -115,7 +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
@ -145,12 +145,12 @@ 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
@ -205,7 +205,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
@ -282,7 +282,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
@ -327,7 +327,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):
@ -421,7 +421,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``
"""

View file

@ -14,7 +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
@ -65,7 +65,7 @@ def _get_scans_dir(config: dict) -> Optional[str]:
def serve_from_cli(argv: Optional[list[str]] = None):
"""Start the server from the command line"""
"""Start the server from the command line."""
args = lt.cli.parse_args(argv)
log_config = copy(uvicorn.config.LOGGING_CONFIG)

View file

@ -25,7 +25,7 @@ def add_static_file(app: FastAPI, fname: str, folder: str) -> None:
def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None:
"""Add the static files responsible for the webapp app to the FastAPI app
"""Add the static files responsible for the webapp app to the FastAPI app.
app: The FastAPI app to add to, in this case the OpenFlexure server
"""

View file

@ -22,7 +22,7 @@ class RecentringThing(lt.Thing):
max_steps=15,
lateral_distance=5000,
):
"""Recentre the stage, based on the focal plane
"""Recentre the stage, based on the focal plane.
Autofocuses at multiple points around the sample to
find the overall maximum (or minimum) height, which

View file

@ -1,4 +1,4 @@
"""OpenFlexure Microscope autofocus module
"""OpenFlexure Microscope autofocus module.
This module defines a Thing that is responsible for using the stage and
camera together to perform an autofocus routine.
@ -26,7 +26,7 @@ from .stage import StageDependency as Stage
class StackParams:
"""A class for holding for scan parameters
"""A class for holding for scan parameters.
All arguments are keyword only
@ -97,7 +97,7 @@ class StackParams:
@property
def stack_z_range(self) -> int:
"""The range of the z stack, in steps
"""The range of the z stack, in steps.
Note that this is the range of the minimum number of images captured,
which is also the range of the images stored in memory that can be
@ -107,7 +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
@ -116,14 +116,14 @@ class StackParams:
@property
def max_images_to_test(self) -> int:
"""The maximum number of images that will be captured and tested in a stack
"""The maximum number of images that will be captured and tested in a stack.
This is 15 images more then the minimum number that are captured.
"""
return self.min_images_to_test + 15
def slice_to_save(self, sharpest_index):
"""Return the slice of images to save given the index of the sharpest image"""
"""Return the slice of images to save given the index of the sharpest image."""
images_each_side = (self.images_to_save - 1) // 2
return slice(
max(sharpest_index - images_each_side, 0),
@ -133,7 +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]
@ -141,7 +141,7 @@ class CaptureInfo:
@property
def filename(self) -> str:
"""The filename for this image generated from the position"""
"""The filename for this image generated from the position."""
return f"{self.position['x']}_{self.position['y']}_{self.position['z']}.jpeg"
@ -195,7 +195,7 @@ class JPEGSharpnessMonitor:
running = False
async def monitor_sharpness(self):
"""Start monitoring the frame sizes"""
"""Start monitoring the frame sizes."""
self.running = True
async for frame in self.camera.lores_mjpeg_stream.frame_async_generator():
self.jpeg_times.append(time.time())
@ -205,7 +205,7 @@ class JPEGSharpnessMonitor:
@contextmanager
def run(self):
"""Context manager, during which we will monitor sharpness from the camera"""
"""Context manager, during which we will monitor sharpness from the camera."""
self.portal.start_task_soon(self.monitor_sharpness)
try:
yield
@ -233,7 +233,7 @@ class JPEGSharpnessMonitor:
def move_data(
self, istart: int, istop: Optional[int] = None
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Extract sharpness as a function of (interpolated) z"""
"""Extract sharpness as a function of (interpolated) z."""
if istop is None:
istop = istart + 2
jpeg_times: np.ndarray = np.array(self.jpeg_times)
@ -261,7 +261,7 @@ class JPEGSharpnessMonitor:
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
def sharpest_z_on_move(self, index: int) -> int:
"""Return the z position of the sharpest image on a given move"""
"""Return the z position of the sharpest image on a given move."""
_, jz, js = self.move_data(index)
if len(js) == 0:
raise ValueError(
@ -270,7 +270,7 @@ class JPEGSharpnessMonitor:
return jz[np.argmax(js)]
def data_dict(self) -> SharpnessDataArrays:
"""Return the gathered data as a single convenient dictionary"""
"""Return the gathered data as a single convenient dictionary."""
data = {}
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
data[k] = getattr(self, k)
@ -295,7 +295,7 @@ class AutofocusThing(lt.Thing):
dz: int = 2000,
start: str = "centre",
) -> SharpnessDataArrays:
"""Sweep the stage up and down, then move to the sharpest point
"""Sweep the stage up and down, then move to the sharpest point.
This method will will move down by dz/2, sweep up by dz, and then evaluate
the position where the image was sharpest. We'll then move back down, and
@ -325,7 +325,7 @@ class AutofocusThing(lt.Thing):
dz: Sequence[int],
wait: float = 0,
) -> SharpnessDataArrays:
"""Make a move (or a series of moves) and monitor sharpness
"""Make a move (or a series of moves) and monitor sharpness.
This method will will make a series of relative moves in z, and
return the sharpness (JPEG size) vs time, along with timestamps
@ -561,7 +561,7 @@ class AutofocusThing(lt.Thing):
cam: WrappedCamera,
stage: Stage,
) -> tuple[bool, list[CaptureInfo], Optional[int]]:
"""Capture a series of images checking that sharpest image central
"""Capture a series of images checking that sharpest image central.
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

View file

@ -22,7 +22,7 @@ class BackgroundDetectThing(lt.Thing):
@lt.thing_setting
def background_distributions(self) -> Optional[ChannelDistributions]:
"""The statistics of the background image"""
"""The statistics of the background image."""
bd = self._background_distributions
if bd is None:
return None
@ -56,7 +56,7 @@ class BackgroundDetectThing(lt.Thing):
)
def background_mask(self, image: np.ndarray) -> np.ndarray:
"""Calculate a binary image, showing whether each pixel is background
"""Calculate a binary image, showing whether each pixel is background.
The image should be in LUV format, the ouput will be binary with the
same shape in the first two dimensions.
@ -78,7 +78,7 @@ class BackgroundDetectThing(lt.Thing):
@lt.thing_action
def background_fraction(self, cam: CamDep) -> float:
"""Determine what fraction of the current image is background
"""Determine what fraction of the current image is background.
This action will acquire a new image from the preview stream, then
evaluate whether it is foreground or background, by comparing it
@ -96,7 +96,7 @@ class BackgroundDetectThing(lt.Thing):
@lt.thing_action
def image_is_sample(self, cam: CamDep) -> bool:
"""Label the current image as either background or sample"""
"""Label the current image as either background or sample."""
b_fraction = self.background_fraction(cam)
fraction_threshold = self.fraction
@ -104,7 +104,7 @@ class BackgroundDetectThing(lt.Thing):
@lt.thing_action
def set_background(self, cam: CamDep):
"""Grab an image, and use its statistics to set the background
"""Grab an image, and use its statistics to set the background.
This should be run when the microscope is looking at an empty region,
and will calculate the mean and standard deviation of the pixel values

View file

@ -1,4 +1,4 @@
"""OpenFlexure Microscope Camera
"""OpenFlexure Microscope Camera.
This module defines the interface for cameras. Any compatible lt.Thing
should enabe the server to work.
@ -23,23 +23,23 @@ class JPEGBlob(lt.blob.Blob):
class PNGBlob(lt.blob.Blob):
"""A class representing a PNG image as a LabThings FastAPI Blob"""
"""A class representing a PNG image as a LabThings FastAPI Blob."""
media_type: str = "image/png"
class ArrayModel(RootModel):
"""A model for an array"""
"""A model for an array."""
root: NDArray
class CaptureError(RuntimeError):
"""An error trying to capture from a CameraThing"""
"""An error trying to capture from a CameraThing."""
class NoImageInMemoryError(RuntimeError):
"""An error called if no image in in memory when an method is called to use that image"""
"""An error called if no image in in memory when an method is called to use that image."""
class CameraMemoryBuffer:
@ -61,7 +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
@ -114,7 +114,7 @@ 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:
@ -140,7 +140,7 @@ class CameraMemoryBuffer:
class BaseCamera(lt.Thing):
"""The base class for all cameras. All cameras must directly inherit from this class"""
"""The base class for all cameras. All cameras must directly inherit from this class."""
mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
@ -181,7 +181,7 @@ class BaseCamera(lt.Thing):
@lt.thing_property
def stream_active(self) -> bool:
"""Whether the MJPEG stream is active"""
"""Whether the MJPEG stream is active."""
raise NotImplementedError(
"CameraThings must define their own stream_active method"
)
@ -203,7 +203,7 @@ class BaseCamera(lt.Thing):
resolution: Literal["lores", "main", "full"] = "main",
wait: Optional[float] = 5,
) -> JPEGBlob:
"""Acquire one image from the camera and return as a JPEG blob"""
"""Acquire one image from the camera and return as a JPEG blob."""
raise NotImplementedError(
"CameraThings must define their own capture_jpeg method"
)
@ -214,7 +214,7 @@ class BaseCamera(lt.Thing):
portal: lt.deps.BlockingPortal,
stream_name: Literal["main", "lores"] = "main",
) -> JPEGBlob:
"""Acquire one image from the preview stream and return as an array
"""Acquire one image from the preview stream and return as an array.
This differs from ``capture_jpeg`` in that it does not pause the MJPEG
preview stream. Instead, we simply return the next frame from that
@ -233,7 +233,7 @@ class BaseCamera(lt.Thing):
portal: lt.deps.BlockingPortal,
stream_name: Literal["main", "lores"] = "main",
) -> int:
"""Acquire one image from the preview stream and return its size"""
"""Acquire one image from the preview stream and return its size."""
stream = (
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
)
@ -245,7 +245,7 @@ class BaseCamera(lt.Thing):
stream_name: Literal["main", "lores", "raw"],
wait: Optional[float],
) -> None:
"""Capture a PIL image from stream stream_name with timeout wait"""
"""Capture a PIL image from stream stream_name with timeout wait."""
raise NotImplementedError(
"CameraThings must define their own capture_image method"
)
@ -258,7 +258,7 @@ class BaseCamera(lt.Thing):
metadata_getter: lt.deps.GetThingStates,
save_resolution: Optional[Tuple[int, int]] = None,
) -> None:
"""Capture an image and save it to disk
"""Capture an image and save it to disk.
:param jpeg_path: The path to save the file to
:param logger: This should be injected automatically by Labthings FastAPI
@ -288,7 +288,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.
@ -336,7 +336,7 @@ class BaseCamera(lt.Thing):
@lt.thing_action
def clear_buffers(self) -> None:
"""Clear all images in memory"""
"""Clear all images in memory."""
self._memory_buffer.clear()
def _robust_image_capture(

View file

@ -1,4 +1,4 @@
"""OpenFlexure Microscope OpenCV Camera
"""OpenFlexure Microscope OpenCV Camera.
This module defines a camera Thing that uses OpenCV's
``VideoCapture``.
@ -23,7 +23,7 @@ from . import BaseCamera, JPEGBlob
class OpenCVCamera(BaseCamera):
"""A Thing representing an OpenCV camera"""
"""A Thing representing an OpenCV camera."""
def __init__(self, camera_index: int = 0):
self.camera_index = camera_index
@ -45,7 +45,7 @@ class OpenCVCamera(BaseCamera):
@lt.thing_property
def stream_active(self) -> bool:
"""Whether the MJPEG stream is active"""
"""Whether the MJPEG stream is active."""
if self._capture_enabled and self._capture_thread:
return self._capture_thread.is_alive()
return False
@ -71,7 +71,7 @@ class OpenCVCamera(BaseCamera):
self,
resolution: Literal["main", "full"] = "full",
) -> NDArray:
"""Acquire one image from the camera and return as an array
"""Acquire one image from the camera and return as an array.
This function will produce a nested list containing an uncompressed RGB image.
It's likely to be highly inefficient - raw and/or uncompressed captures using
@ -91,7 +91,7 @@ class OpenCVCamera(BaseCamera):
metadata_getter: lt.deps.GetThingStates,
resolution: Literal["main", "full"] = "main",
) -> JPEGBlob:
"""Acquire one image from the camera and return as a JPEG blob
"""Acquire one image from the camera and return as a JPEG blob.
This function will produce a JPEG image.
"""

View file

@ -40,10 +40,10 @@ from . import BaseCamera, JPEGBlob, ArrayModel
class PicameraStreamOutput(Output):
"""An Output class that sends frames to a stream"""
"""An Output class that sends frames to a stream."""
def __init__(self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal):
"""Create an output that puts frames in an MJPEGStream
"""Create an output that puts frames in an MJPEGStream.
We need to pass the stream object, and also the blocking portal, because
new frame notifications happen in the anyio event loop and frames are
@ -57,7 +57,7 @@ class PicameraStreamOutput(Output):
def outputframe(
self, frame, _keyframe=True, _timestamp=None, _packet=None, _audio=False
):
"""Add a frame to the stream's ringbuffer"""
"""Add a frame to the stream's ringbuffer."""
self.stream.add_frame(frame, self.portal)
@ -104,7 +104,7 @@ class LensShading(BaseModel):
class StreamingPiCamera2(BaseCamera):
"""A Thing that represents an OpenCV camera"""
"""A Thing that represents an OpenCV camera."""
def __init__(self, camera_num: int = 0):
self._setting_save_in_progress = False
@ -244,7 +244,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def sensor_modes(self) -> list[SensorMode]:
"""All the available modes the current sensor supports"""
"""All the available modes the current sensor supports."""
if not self._sensor_modes:
with self._streaming_picamera() as cam:
self._sensor_modes = cam.sensor_modes
@ -254,14 +254,14 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def sensor_mode(self) -> Optional[SensorModeSelector]:
"""The intended sensor mode of the camera"""
"""The intended sensor mode of the camera."""
if self._sensor_mode is None:
return None
return SensorModeSelector(**self._sensor_mode)
@sensor_mode.setter
def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]):
"""Change the sensor mode used"""
"""Change the sensor mode used."""
if new_mode is None:
self._sensor_mode = None
elif isinstance(new_mode, SensorModeSelector):
@ -277,7 +277,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def sensor_resolution(self) -> Optional[tuple[int, int]]:
"""The native resolution of the camera's sensor"""
"""The native resolution of the camera's sensor."""
with self._streaming_picamera() as cam:
return cam.sensor_resolution
@ -324,7 +324,7 @@ class StreamingPiCamera2(BaseCamera):
@property
def streaming(self) -> bool:
"""True if the camera is streaming"""
"""True if the camera is streaming."""
return self._picamera is not None and self._picamera.started
@contextmanager
@ -432,7 +432,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
@ -474,7 +474,7 @@ class StreamingPiCamera2(BaseCamera):
stream_name: Literal["main", "lores", "raw", "full"] = "main",
wait: Optional[float] = 0.9,
) -> ArrayModel:
"""Acquire one image from the camera and return as an array
"""Acquire one image from the camera and return as an array.
This function will produce a nested list containing an uncompressed RGB image.
It's likely to be highly inefficient - raw and/or uncompressed captures using
@ -496,7 +496,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def camera_configuration(self) -> Mapping:
"""The "configuration" dictionary of the picamera2 object
"""The "configuration" dictionary of the picamera2 object.
The "configuration" sets the resolution and format of the camera's streams.
Together with the "tuning" it determines how the sensor is configured and
@ -516,7 +516,7 @@ class StreamingPiCamera2(BaseCamera):
resolution: Literal["lores", "main", "full"] = "main",
wait: Optional[float] = 0.9,
) -> JPEGBlob:
"""Acquire one image from the camera as a JPEG
"""Acquire one image from the camera as a JPEG.
The JPEG will be acquired using ``Picamera2.capture_file``. If the
``resolution`` parameter is ``main`` or ``lores``, it will be captured
@ -567,7 +567,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def capture_metadata(self) -> dict:
"""Return the metadata from the camera"""
"""Return the metadata from the camera."""
with self._streaming_picamera() as cam:
return cam.capture_metadata()
@ -577,7 +577,7 @@ class StreamingPiCamera2(BaseCamera):
target_white_level: int = 700,
percentile: float = 99.9,
):
"""Adjust exposure until a the target white level is reached
"""Adjust exposure until a the target white level is reached.
Starting from the minimum exposure, gradually increase exposure until
the image reaches the specified white level.
@ -603,7 +603,7 @@ class StreamingPiCamera2(BaseCamera):
method: Literal["percentile", "centre"] = "centre",
luminance_power: float = 1.0,
):
"""Correct the white balance of the image
"""Correct the white balance of the image.
This calibration requires a neutral image, such that the 99th centile
of each colour channel should correspond to white. We calculate the
@ -711,7 +711,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_action
def full_auto_calibrate(self) -> None:
"""Perform a full auto-calibration
"""Perform a full auto-calibration.
This function will call the other calibration actions in sequence:
@ -729,7 +729,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_action
def flat_lens_shading(self) -> None:
"""Disable flat-field correction
"""Disable flat-field correction.
This method will set a completely flat lens shading table. It is not the
same as the default behaviour, which is to use an adaptive lens shading
@ -748,7 +748,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def lens_shading_tables(self) -> Optional[LensShading]:
"""The current lens shading (i.e. flat-field correction)
"""The current lens shading (i.e. flat-field correction).
Return the current lens shading correction, as three 2D lists each with
dimensions 16x12, if a static lens shading table is in use.
@ -770,7 +770,7 @@ class StreamingPiCamera2(BaseCamera):
return None
def reshape_lst(lin: list[float]) -> list[list[float]]:
"""Reshape the 192 element list into a 2D 16x12 list"""
"""Reshape the 192 element list into a 2D 16x12 list."""
w, h = 16, 12
return [lin[w * i : w * (i + 1)] for i in range(h)]
@ -782,7 +782,7 @@ class StreamingPiCamera2(BaseCamera):
@lens_shading_tables.setter
def lens_shading_tables(self, lst: LensShading) -> None:
"""Set the lens shading tables"""
"""Set the lens shading tables."""
with self._streaming_picamera(pause_stream=True):
recalibrate_utils.set_static_lst(
self.tuning,
@ -795,7 +795,7 @@ class StreamingPiCamera2(BaseCamera):
def correct_colour_gains_for_lens_shading(
self, colour_gains: tuple[float, float]
) -> tuple[float, float]:
"""Correct white balance gains for the effect of lens shading
"""Correct white balance gains for the effect of lens shading.
The white balance algorithm we use assumes the brightest pixels
should be white, and that the only thing affecting the colour of
@ -825,7 +825,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_action
def flat_lens_shading_chrominance(self) -> None:
"""Disable flat-field correction
"""Disable flat-field correction.
This method will set the chrominance of the lens shading table to be
flat, i.e. we'll correct vignetting of intensity, but not any change in
@ -840,7 +840,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_action
def reset_lens_shading(self) -> None:
"""Revert to default lens shading settings
"""Revert to default lens shading settings.
This method will restore the default "adaptive" lens shading method used
by the Raspberry Pi camera.
@ -851,7 +851,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def lens_shading_is_static(self) -> bool:
"""Whether the lens shading is static
"""Whether the lens shading is static.
This property is true if the lens shading correction has been set to use
a static table (i.e. the number of automatic correction iterations is zero).

View file

@ -1,4 +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
@ -52,7 +52,7 @@ LensShadingTables = tuple[np.ndarray, np.ndarray, np.ndarray]
def load_default_tuning(cam: Picamera2) -> dict:
"""Load the default tuning file for the camera
"""Load the default tuning file for the camera.
This will open and close the camera to determine its model. If you are
using a model that's supported by ``picamera2`` it should have a tuning
@ -76,7 +76,7 @@ def load_default_tuning(cam: Picamera2) -> dict:
def set_minimum_exposure(camera: Picamera2) -> None:
"""Enable manual exposure, with low gain and shutter speed
"""Enable manual exposure, with low gain and shutter speed.
We set exposure mode to manual, analog and digital gain
to 1, and shutter speed to the minimum (8us for Pi Camera v2)
@ -93,7 +93,7 @@ def set_minimum_exposure(camera: Picamera2) -> None:
class ExposureTest(BaseModel):
"""Record the results of testing the camera's current exposure settings"""
"""Record the results of testing the camera's current exposure settings."""
level: int
exposure_time: int
@ -101,7 +101,7 @@ class ExposureTest(BaseModel):
def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest:
"""Evaluate current exposure settings using a raw image
"""Evaluate current exposure settings using a raw image.
CAMERA SHOULD BE STARTED!
@ -136,7 +136,7 @@ def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest
def check_convergence(test: ExposureTest, target: int, tolerance: float) -> bool:
"""Check whether the brightness is within the specified target range"""
"""Check whether the brightness is within the specified target range."""
return abs(test.level - target) < target * tolerance
@ -321,7 +321,7 @@ def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
"""Compresses channel down to a 16x12 grid - from libcamera
"""Compresses channel down to a 16x12 grid - from libcamera.
This is taken from
https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
@ -343,7 +343,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
"""Zoom an image in the last two dimensions
"""Zoom an image in the last two dimensions.
This is effectively the inverse operation of ``get_16x12_grid``
"""
@ -354,7 +354,7 @@ def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray]:
"""Generate a downsampled, un-normalised image from which to calculate the LST
"""Generate a downsampled, un-normalised image from which to calculate the LST.
TODO: blacklevel probably ought to be determined from the camera...
"""
@ -382,7 +382,7 @@ def lst_from_channels(channels: np.ndarray) -> LensShadingTables:
def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
"""Given 4 downsampled grids, generate the luminance and chrominance tables
"""Given 4 downsampled grids, generate the luminance and chrominance tables.
The grids are the 4 BAYER channels RGGB
@ -408,7 +408,7 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
def grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarray:
"""Convert form luminance/chrominance dict to four RGGB channels
"""Convert form luminance/chrominance dict to four RGGB channels.
Note that these will be normalised - the maximum green value is always 1.
Also, note that the channels are BGGR, to be consistent with the
@ -462,13 +462,13 @@ def set_static_ccm(
def get_static_ccm(tuning: dict) -> None:
"""Get the ``rpi.ccm`` section of a camera tuning dict"""
"""Get the ``rpi.ccm`` section of a camera tuning dict."""
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
return ccm["ccms"]
def lst_is_static(tuning: dict) -> bool:
"""Whether the lens shading table is set to static"""
"""Whether the lens shading table is set to static."""
alsc = Picamera2.find_tuning_algo(tuning, "rpi.alsc")
return alsc["n_iter"] == 0
@ -493,13 +493,13 @@ def set_static_geq(
def _geq_is_static(tuning: dict) -> bool:
"""Whether the green equalisation is set to static"""
"""Whether the green equalisation is set to static."""
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
return geq["offset"] == 65535
def index_of_algorithm(algorithms: list[dict], algorithm: str) -> int:
"""Find the index of an algorithm's section in the tuning file"""
"""Find the index of an algorithm's section in the tuning file."""
for i, a in enumerate(algorithms):
if algorithm in a:
return i
@ -557,5 +557,5 @@ def recreate_camera_manager() -> None:
def as_flat_rounded_list(array: np.ndarray, round_to: int = 3) -> list[float]:
"""Flatten array, round, and then convert to list"""
"""Flatten array, round, and then convert to list."""
return np.reshape(array, -1).round(round_to).tolist()

View file

@ -1,4 +1,4 @@
"""OpenFlexure Microscope OpenCV Camera
"""OpenFlexure Microscope OpenCV Camera.
This module defines a Thing that is responsible for using the stage and
camera together to perform an autofocus routine.
@ -30,7 +30,7 @@ RATIO = 0.2
class SimulatedCamera(BaseCamera):
"""A Thing representing an OpenCV camera"""
"""A Thing representing an OpenCV camera."""
_stage: Optional[BaseStage] = None
_server: Optional[lt.ThingServer] = None
@ -53,7 +53,7 @@ class SimulatedCamera(BaseCamera):
self.generate_canvas()
def generate_sprites(self):
"""Generate sprites to populate the image"""
"""Generate sprites to populate the image."""
self.sprites = []
black = np.zeros(self.glyph_shape, dtype=np.uint8)
x = np.arange(black.shape[0])
@ -65,7 +65,7 @@ class SimulatedCamera(BaseCamera):
self.sprites.append(sprite)
def generate_blobs(self, n_blobs: int = 1000):
"""Generate coordinates of blobs
"""Generate coordinates of blobs.
Blobs are characterised by X, Y, sprite
We also generate a KD tree to rapidly find blobs in an image
@ -78,7 +78,7 @@ class SimulatedCamera(BaseCamera):
self.blobs[:, 2] = rng.choice(len(self.sprites), n_blobs)
def generate_canvas(self):
"""Generate a blank canvas"""
"""Generate a blank canvas."""
self.canvas = np.zeros(self.canvas_shape, dtype=np.uint8)
self.canvas[...] = 255
w, h, _ = self.glyph_shape
@ -89,7 +89,7 @@ class SimulatedCamera(BaseCamera):
] -= self.sprites[int(sprite)]
def generate_image(self, pos: tuple[int, int, int]):
"""Generate an image with blobs based on supplied coordinates"""
"""Generate an image with blobs based on supplied coordinates."""
canvas_width, canvas_height, _ = self.canvas_shape
image_width, image_height, _ = self.shape
pos = tuple(x * RATIO for x in pos)
@ -124,7 +124,7 @@ class SimulatedCamera(BaseCamera):
return self._stage.instantaneous_position
def generate_frame(self):
"""Generate a frame with blobs based on the stage coordinates"""
"""Generate a frame with blobs based on the stage coordinates."""
try:
pos = self.get_stage_position()
except Exception as e:
@ -145,7 +145,7 @@ class SimulatedCamera(BaseCamera):
@lt.thing_property
def stream_active(self) -> bool:
"""Whether the MJPEG stream is active"""
"""Whether the MJPEG stream is active."""
if self._capture_enabled and self._capture_thread:
return self._capture_thread.is_alive()
return False
@ -170,7 +170,7 @@ class SimulatedCamera(BaseCamera):
self,
resolution: Literal["main", "full"] = "full",
) -> ArrayModel:
"""Acquire one image from the camera and return as an array
"""Acquire one image from the camera and return as an array.
This function will produce a nested list containing an uncompressed RGB image.
It's likely to be highly inefficient - raw and/or uncompressed captures using
@ -185,7 +185,7 @@ class SimulatedCamera(BaseCamera):
metadata_getter: lt.deps.GetThingStates,
resolution: Literal["main", "full"] = "main",
) -> JPEGBlob:
"""Acquire one image from the camera and return as a JPEG blob
"""Acquire one image from the camera and return as a JPEG blob.
This function will produce a JPEG image.
"""

View file

@ -1,4 +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
@ -53,7 +53,7 @@ class HardwareInterfaceModel(BaseModel):
def downsample(factor: int, image: np.ndarray) -> np.ndarray:
"""Downsample an image by taking the mean of each nxn region
"""Downsample an image by taking the mean of each nxn region.
This should be very efficient: we calculate the mean of each
``factor * factor`` square, no interpolation. If the image is
@ -79,7 +79,7 @@ DEFAULT_SETTLING_TIME = 0.2
def make_hardware_interface(
stage: Stage, camera: Camera, downsample_factor: int = 2
) -> HardwareInterfaceModel:
"""Construct the functions we need to interface with the hardware"""
"""Construct the functions we need to interface with the hardware."""
axes = stage.axis_names
def pos2dict(pos: Sequence[float]) -> Mapping[str, float]:
@ -144,7 +144,7 @@ class LoggingMoveWrapper:
self.clear_history()
def __call__(self, new_position: CoordinateType, *args, **kwargs):
"""Move to a new position, and record it"""
"""Move to a new position, and record it."""
self._history.append((time.time(), self._current_position))
self._move_function(new_position, *args, **kwargs)
self._current_position = new_position
@ -152,13 +152,13 @@ class LoggingMoveWrapper:
@property
def history(self) -> MoveHistory:
"""The history, as a numpy array of times and another of positions"""
"""The history, as a numpy array of times and another of positions."""
times: List[float] = [t for t, p in self._history if p is not None]
positions: List[CoordinateType] = [p for t, p in self._history if p is not None]
return MoveHistory(times, positions)
def clear_history(self):
"""Reset our history to be an empty list"""
"""Reset our history to be an empty list."""
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
@ -175,7 +175,7 @@ class CSMUncalibratedError(HTTPException):
class CameraStageMapper(lt.Thing):
"""A Thing to manage mapping between image and stage coordinates"""
"""A Thing to manage mapping between image and stage coordinates."""
@lt.thing_action
def calibrate_1d(
@ -185,7 +185,7 @@ class CameraStageMapper(lt.Thing):
logger: lt.deps.InvocationLogger,
direction: Tuple[float, float, float],
) -> DenumpifyingDict:
"""Move a microscope's stage in 1D, and figure out the relationship with the camera"""
"""Move a microscope's stage in 1D, and figure out the relationship with the camera."""
move = LoggingMoveWrapper(
hw.move
) # log positions and times for stage calibration
@ -214,7 +214,7 @@ class CameraStageMapper(lt.Thing):
def calibrate_xy(
self, hw: HardwareInterfaceDep, stage: Stage, logger: lt.deps.InvocationLogger
) -> DenumpifyingDict:
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera.
This performs two 1d calibrations in x and y, then combines their results.
"""
@ -291,13 +291,13 @@ class CameraStageMapper(lt.Thing):
@lt.thing_property
def image_resolution(self) -> Optional[Tuple[float, float]]:
"""The image size used to calibrate the image_to_stage_displacement_matrix"""
"""The image size used to calibrate the image_to_stage_displacement_matrix."""
if self.last_calibration is None:
return None
return self.last_calibration["image_resolution"]
def assert_calibrated(self):
"""Raise an exception if the image_to_stage_displacement matrix is not set"""
"""Raise an exception if the image_to_stage_displacement matrix is not set."""
if self.image_to_stage_displacement_matrix is None:
# Disable check of no message in raised exception as the message is explicitly
# added by CSMUncalibratedError
@ -310,7 +310,7 @@ class CameraStageMapper(lt.Thing):
x: float,
y: float,
):
"""Move by a given number of pixels on the camera
"""Move by a given number of pixels on the camera.
NB x and y here refer to what is usually understood to be the horizontal and
vertical axes of the image. In many toolkits, "matrix indices" are used, which
@ -329,7 +329,7 @@ class CameraStageMapper(lt.Thing):
@lt.thing_property
def thing_state(self) -> dict[str, Any]:
"""Summary metadata describing the current state of the Thing"""
"""Summary metadata describing the current state of the Thing."""
return {
k: getattr(self, k)
for k in ["image_to_stage_displacement_matrix", "image_resolution"]

View file

@ -1,4 +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
@ -21,7 +21,7 @@ ThingServerDep = Annotated[lt.ThingServer, Depends(thing_server_from_request)]
def recursive_update(old_dict: MutableMapping, update: Mapping):
"""Update a dictionary recursively"""
"""Update a dictionary recursively."""
for k, v in update.items():
if isinstance(v, Mapping):
if k in old_dict and isinstance(old_dict[k], MutableMapping):
@ -33,7 +33,7 @@ def recursive_update(old_dict: MutableMapping, update: Mapping):
def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any:
"""Index a dict-of-dicts with a sequence of strings"""
"""Index a dict-of-dicts with a sequence of strings."""
subdict = data
for k in key:
if k not in subdict and create:
@ -100,7 +100,7 @@ class SettingsManager(lt.Thing):
@lt.thing_setting
def microscope_id(self) -> UUID:
"""A unique identifier for this microscope"""
"""A unique identifier for this microscope."""
if self._microscope_id is None:
self._microscope_id = str(uuid4())
return UUID(self._microscope_id)
@ -117,7 +117,7 @@ class SettingsManager(lt.Thing):
@lt.thing_action
def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping:
"""Metadata summarising the current state of all Things in the server"""
"""Metadata summarising the current state of all Things in the server."""
return metadata_getter()
@property

View file

@ -42,7 +42,7 @@ STITCHING_RESOLUTION = (820, 616)
class ScanNotRunningError(RuntimeError):
"""Exception called when scan not running that requires a scan to be running"""
"""Exception called when scan not running that requires a scan to be running."""
def _scan_running(method):
@ -170,7 +170,7 @@ class SmartScanThing(lt.Thing):
@_scan_running
def _check_background_and_csm_set(self):
"""Before starting a scan, check that background and camera-stage-mapping are set
"""Before starting a scan, check that background and camera-stage-mapping are set.
Raise error if:
- background is to be skipped but is not set
@ -229,7 +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%.
@ -520,7 +520,7 @@ class SmartScanThing(lt.Thing):
@_scan_running
def _return_to_starting_position(self):
"""Return to the initial scan position, if set"""
"""Return to the initial scan position, if set."""
self._scan_logger.info("Returning to starting position.")
if self._starting_position is not None:
self._stage.move_absolute(
@ -529,7 +529,7 @@ class SmartScanThing(lt.Thing):
@_scan_running
def _perform_final_stitch(self):
"""Update the scan zip and perform final stitch of the data"""
"""Update the scan zip and perform final stitch of the data."""
if self._scan_images_taken <= 3:
self._scan_logger.info("Not performing a stitch as 3 or fewer images taken")
return
@ -626,7 +626,7 @@ class SmartScanThing(lt.Thing):
@lt.thing_property
def scans(self) -> list[scan_directories.ScanInfo]:
"""All the available scans
"""All the available scans.
Each scan has a name (which can be used to access it), along with
its modified and created times (according to the filesystem) and
@ -687,7 +687,7 @@ class SmartScanThing(lt.Thing):
"scans",
)
def delete_all_scans(self, logger: lt.deps.InvocationLogger) -> None:
"""Delete all the scans on the microscope
"""Delete all the scans on the microscope.
**This will irreversibly remove all scanned data from the
microscope!**
@ -698,7 +698,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:
@ -722,7 +722,7 @@ class SmartScanThing(lt.Thing):
@property
def latest_preview_stitch_path(self) -> Optional[str]:
"""The path of the latest preview stitched image, or None if not available"""
"""The path of the latest preview stitched image, or None if not available."""
if not self.latest_scan_name:
return None
@ -732,7 +732,7 @@ class SmartScanThing(lt.Thing):
@lt.thing_property
def latest_preview_stitch_time(self) -> Optional[float]:
"""The modification time of the latest preview image, to allow live updating
"""The modification time of the latest preview image, to allow live updating.
This will return None (``null`` to JS) if there is no preview image to return.
@ -795,7 +795,7 @@ class SmartScanThing(lt.Thing):
@_scan_running
def _preview_stitch_running(self) -> bool:
"""Whether there is a preview stitch running in a subprocess"""
"""Whether there is a preview stitch running in a subprocess."""
with self._preview_stitch_popen_lock:
if self._preview_stitch_popen is None:
return False
@ -805,7 +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()
@ -816,7 +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
@ -870,7 +870,7 @@ class SmartScanThing(lt.Thing):
stitch_resize: Optional[float] = None,
overlap: float = 0.0,
) -> None:
"""Generate a stitched image based on stage position metadata
"""Generate a stitched image based on stage position metadata.
Note that as this is a lt.thing_action it needs the logger passed as
a variable if called from another thing action

View file

@ -5,7 +5,7 @@ import labthings_fastapi as lt
class BaseStage(lt.Thing):
"""A base stage class for OpenFlexure translation stages
"""A base stage class for OpenFlexure translation stages.
This can't be used directly but should reduce boilerplate code when
implementing new stages. A minimal working stage must implement
@ -38,7 +38,7 @@ class BaseStage(lt.Thing):
@property
def thing_state(self):
"""Summary metadata describing the current state of the stage"""
"""Summary metadata describing the current state of the stage."""
return {"position": self.position}
@lt.thing_action
@ -67,7 +67,7 @@ class BaseStage(lt.Thing):
@lt.thing_action
def set_zero_position(self):
"""Make the current position zero in all axes
"""Make the current position zero in all axes.
This action does not move the stage, but resets the position to zero.
It is intended for use after manually or automatically recentring the

View file

@ -9,7 +9,7 @@ from . import BaseStage
class DummyStage(BaseStage):
"""A dummy stage for testing purposes
"""A dummy stage for testing purposes.
This stage should work similarly to a Sangaboard stage, but without any
hardware attached.
@ -83,7 +83,7 @@ class DummyStage(BaseStage):
@lt.thing_action
def set_zero_position(self):
"""Make the current position zero in all axes
"""Make the current position zero in all axes.
This action does not move the stage, but resets the position to zero.
It is intended for use after manually or automatically recentring the

View file

@ -13,7 +13,7 @@ from . import BaseStage
class SangaboardThing(BaseStage):
"""A Thing to manage a Sangaboard motor controller
"""A Thing to manage a Sangaboard motor controller.
Internally, this uses the ``pysangaboard`` package from PyPi. This imports
as ``sangaboard``. As ``pysangaboard`` does not support some features added
@ -115,7 +115,7 @@ class SangaboardThing(BaseStage):
@lt.thing_action
def set_zero_position(self) -> None:
"""Make the current position zero in all axes
"""Make the current position zero in all axes.
This action does not move the stage, but resets the position to zero.
It is intended for use after manually or automatically recentring the
@ -132,7 +132,7 @@ class SangaboardThing(BaseStage):
dt: float = 0.5,
led_channel: Literal["cc"] = "cc",
) -> None:
"""Flash the LED to identify the board
"""Flash the LED to identify the board.
This is intended to be useful in situations where there are multiple
Sangaboards in use, and it is necessary to identify which one is

View file

@ -1,4 +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.
"""
@ -15,11 +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,
@ -36,7 +36,7 @@ class SystemControlThing(lt.Thing):
@lt.thing_action
def reboot(self) -> CommandOutput:
"""Attempt to reboot the device"""
"""Attempt to reboot the device."""
p = subprocess.Popen(
["sudo", "shutdown", "-r", "now"],
stderr=subprocess.PIPE,

View file

@ -17,7 +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 = ()