Single line summaries of docstrings

This commit is contained in:
Julian Stirling 2025-07-10 01:58:14 +01:00
parent 35d47fe3ed
commit 4dc41bb008
20 changed files with 153 additions and 115 deletions

View file

@ -1,8 +1,5 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
"""Check that the npm version string matches the Python version string exactly."""
"""A script to check that the npm version string matches the python
version string exactly.
"""
import json import json
import tomllib import tomllib

View file

@ -32,8 +32,9 @@ def test_calibration(client):
def test_jpeg_and_array(client): def test_jpeg_and_array(client):
"""Check that grabbing a jpeg from the stream results in the same size """Check that a jpeg grabbed from the stream is the same size as other captures.
image as a array capture or a jpeg capture.
Compare it to an array capture and a jpeg capture.
""" """
# Grab a jpeg from the stream # Grab a jpeg from the stream
blob = client.grab_jpeg() blob = client.grab_jpeg()

View file

@ -46,7 +46,9 @@ def print_tuning(read_file: bool = False):
def _test_bad_tuning_after_good_tuning(configure: bool = False): def _test_bad_tuning_after_good_tuning(configure: bool = False):
"""Load the default tuning file into the camera, re-load with a broken tuning file, """Test loading good, bad, then another good tuning files in sequence.
Load the default tuning file into the camera, re-load with a broken tuning file,
check it errors. Finally check the default tuning file will load again afterwards. check it errors. Finally check the default tuning file will load again afterwards.
:param configure: Boolean, set true to configure the camera on initial loading :param configure: Boolean, set true to configure the camera on initial loading

View file

@ -127,10 +127,9 @@ ignore = [
"E501", # Ignore long lines for now "E501", # Ignore long lines for now
"D203", # incompatible with D204 "D203", # incompatible with D204
"D213", # incompatible with D212 "D213", # incompatible with D212
"D400", # A stricter version of #415
# Below are checkers to be turned on gradually # Below are checkers to be turned on gradually
"D205", # Force single line summary!
"D415", "D415",
"D400",
"D200", "D200",
# These should be turned on before this MR is complete # These should be turned on before this MR is complete
"D100", "D100",

View file

@ -1,8 +1,9 @@
"""Copyright (c) 2020 Bath Open Instrumentation Group """A script for uploading to zenodo.
Copyright (c) 2020 Bath Open Instrumentation Group
Adapted from: https://gitlab.com/schlauch/zenodo-api-test/ Adapted from: https://gitlab.com/schlauch/zenodo-api-test/
Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved. Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved.
SPDX-License-Identifier: MIT-DLR SPDX-License-Identifier: MIT-DLR
""" """
import json import json

View file

@ -68,9 +68,7 @@ def retrieve_log_from_file() -> PlainTextResponse:
class OFMHandler(logging.Handler): class OFMHandler(logging.Handler):
"""A child class of logging.Handler. This class handles storing the most recent """A logging.Handler that stores the most recent logs for access by the server."""
logs for access by the server.
"""
def __init__(self, level=logging.INFO, max_logs=250): def __init__(self, level=logging.INFO, max_logs=250):
super().__init__(level=level) super().__init__(level=level)
@ -78,8 +76,11 @@ class OFMHandler(logging.Handler):
self._max_logs = max_logs self._max_logs = max_logs
def append_record(self, record): def append_record(self, record):
"""Use the built in formatter to format the record, then save """Format message and append it to a list of records.
it to an array. Pop any in excess of the maximum number of logs
The built in formatter is used to format the record.
Any records in excess of the maximum number of logs are removed.
""" """
self._log.append(self.format(record)) self._log.append(self.format(record))
while len(self._log) > self._max_logs: while len(self._log) > self._max_logs:

View file

@ -224,8 +224,9 @@ class ScanDirectory:
@property @property
def images_dir(self) -> Optional[str]: def images_dir(self) -> Optional[str]:
"""The path to the images directory. None is returned if no images directory """The path to the images directory.
was created
None is returned if no images directory was created.
""" """
im_path = os.path.join(self.dir_path, IMG_DIR_NAME) im_path = os.path.join(self.dir_path, IMG_DIR_NAME)
if os.path.isdir(im_path): if os.path.isdir(im_path):

View file

@ -155,8 +155,7 @@ class ScanPlanner:
return tuple(position) in self._remaining_locations return tuple(position) in self._remaining_locations
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]: def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
"""Return the next location to scan, and the estimated z-position """Return the next location to scan and its estimated z-position.
for this location.
Note z-position may be None! This indicates that the current z, position Note z-position may be None! This indicates that the current z, position
should be used. should be used.
@ -176,9 +175,12 @@ class ScanPlanner:
return next_location, z return next_location, z
def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]: def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
"""Return the xyz position of the closest site where focus was achieved """Return the xyz position of the closest site where focus was achieved.
to the input xy_position, with the most recently taken image returned in
the case of a tie The most recently taken image is returned in the case of a tie.
:param xy_pos: The xy_position which the returned position should be closest
to.
Returns None if there if no focussed locations are present Returns None if there if no focussed locations are present
""" """
@ -338,9 +340,10 @@ class SmartSpiral(ScanPlanner):
self._remaining_locations.sort(key=sort_key) self._remaining_locations.sort(key=sort_key)
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]: def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
"""Return the next location to scan, and the estimated z-position """Return the next location to scan and its estimated z-position.
for this location. This overrides the default behaviour of ScanPlanner
to take the lowest value of nearest neighbours as this works best for smart stack This overrides the default behaviour of ScanPlanner to take the lowest value of
nearest neighbours as this works best for smart stack.
Note z-position may be None! This indicates that the current z position Note z-position may be None! This indicates that the current z position
should be used. should be used.
@ -361,9 +364,11 @@ class SmartSpiral(ScanPlanner):
def select_nearby_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]: def select_nearby_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
"""Return the xyz position of the nearby site with the lowest z position. """Return the xyz position of the nearby site with the lowest z position.
Lowest position is best, as starting too high causes smart stacking to Lowest position is best, as starting too high causes smart stacking to
autofocus and restart. Starting too low just requires extra movements in +z. autofocus and restart. Starting too low just requires extra movements in +z.
Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest neighbour. Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest
neighbour.
Returns None if there if no focused locations are present Returns None if there if no focused locations are present
""" """
@ -397,12 +402,10 @@ class SmartSpiral(ScanPlanner):
starting_pos: XYPos | np.ndarray, starting_pos: XYPos | np.ndarray,
ending_pos: XYPos | np.ndarray, ending_pos: XYPos | np.ndarray,
) -> float: ) -> float:
"""Return the number of moves between two xy positions in the x or y direction """Return the larger of x moves or y moves between two xy positions.
whichever is largest
Args: :param starting_pos: the position to measure from
starting_pos: the position to measure from :param ending_pos: the position to measure to
ending_pos: the position to measure to
""" """
move_size = np.array([self._dx, self._dy]) move_size = np.array([self._dx, self._dy])

View file

@ -9,7 +9,8 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
def add_static_file(app: FastAPI, fname: str, folder: str) -> None: def add_static_file(app: FastAPI, fname: str, folder: str) -> None:
"""Add a single file to the root of the FastAPI app """Add a single file to the root of the FastAPI app.
The file with name ``fname`` will be mounted at ``/fname`` - the The file with name ``fname`` will be mounted at ``/fname`` - the
``folder`` does not affect where it is mounted in the app. ``folder`` does not affect where it is mounted in the app.

View file

@ -159,8 +159,7 @@ def _get_capture_by_id(captures: list[CaptureInfo], buffer_id: int) -> CaptureIn
def _get_capture_index_by_id(captures: list[CaptureInfo], buffer_id: int) -> int: def _get_capture_index_by_id(captures: list[CaptureInfo], buffer_id: int) -> int:
"""Return the index of the capture with the matching id from a list of CaptureInfo """Return the index of the capture with the matching id.
objects
:param captures: A list of capture objects :param captures: A list of capture objects
:param buffer_id: The buffer id of the image to return :param buffer_id: The buffer id of the image to return
@ -432,10 +431,13 @@ class AutofocusThing(lt.Thing):
autofocus_dz: int, autofocus_dz: int,
save_resolution: tuple[int, int], save_resolution: tuple[int, int],
) -> tuple[bool, int]: ) -> tuple[bool, int]:
"""Run a smart stack, which captures images offset in z, testing """Run a smart stack.
whether the sharpest image is towards the centre of the stack.
The sharpest image, and optionally images around the sharpest, A smart stack captures images offset in z, testing whether the sharpest image
will be saved using their coordinates to images_dir is towards the centre of the stack.
The sharpest image, and optionally images around the sharpest, will be saved
to the images_dir with their coordinates in the filename.
:param cam: Camera Dependency supplied by LabThings dependency injection :param cam: Camera Dependency supplied by LabThings dependency injection
@ -506,15 +508,13 @@ class AutofocusThing(lt.Thing):
stage: Stage, stage: Stage,
sharpness_monitor: SharpnessMonitorDep, sharpness_monitor: SharpnessMonitorDep,
) -> None: ) -> None:
"""Return to the initial height of the current stack, and run """Return to the initial z position and run a looping autofocus.
a looping autofocus.
:param initial_z_pos: The initial z positions of previous captures :param initial_z_pos: The initial z positions of previous captures
:param autofocus_dz: the range in steps to autofocus :param autofocus_dz: the range in steps to autofocus
``stage`` and ``sharpness_monitor`` are Thing dependencies passed through ``stage`` and ``sharpness_monitor`` are Thing dependencies passed through
from the calling action from the calling action.
""" """
stage.move_absolute(z=initial_z_pos) stage.move_absolute(z=initial_z_pos)
self.looping_autofocus( self.looping_autofocus(
@ -530,8 +530,10 @@ class AutofocusThing(lt.Thing):
stack_parameters: StackParams, stack_parameters: StackParams,
cam: WrappedCamera, cam: WrappedCamera,
) -> int: ) -> int:
"""Save the required captures to disk. Will save the sharpest image, """Save the required captures to disk.
and any images either side of focus.
This will save the sharpest image, and optionally extra images either
side of focus (see ``stack_parameters.images_to_save``).
:param sharpest_id: the buffer id index of the sharpest image :param sharpest_id: the buffer id index of the sharpest image
:param captures: a list of captures, including file name, image data and :param captures: a list of captures, including file name, image data and
@ -559,8 +561,12 @@ class AutofocusThing(lt.Thing):
cam: WrappedCamera, cam: WrappedCamera,
stage: Stage, stage: Stage,
) -> tuple[bool, list[CaptureInfo], Optional[int]]: ) -> tuple[bool, list[CaptureInfo], Optional[int]]:
"""Capture a series of images offset by stack_parameters.stack_dz, and test whether """Capture a series of images checking that sharpest image central
the sharpest image is towards the centre of the stack.
The images are seperated in z offset by stack_parameters.stack_dz, as they
are captured the last stack_parameters.min_images_to_test images are checked
to see if the sharpest image is central enough in the stack. If it is the stack
completes.
:param stack_parameters: a StackParams object holding stack parameters :param stack_parameters: a StackParams object holding stack parameters
:param cam: Camera Dependency to be passed through from the calling action :param cam: Camera Dependency to be passed through from the calling action
@ -644,8 +650,7 @@ class AutofocusThing(lt.Thing):
def check_stack_result( def check_stack_result(
self, captures: list[CaptureInfo] self, captures: list[CaptureInfo]
) -> tuple[Literal["success", "continue", "restart"], int]: ) -> tuple[Literal["success", "continue", "restart"], int]:
"""Test a list of captures, to decide whether the sharpest image from a """Check if the sharpest image in a list of captures is central enough.
stack is centrally enough in the stack
:param captures: a list of the capture objects to for testing if the :param captures: a list of the capture objects to for testing if the
sharpness has converged in the centre sharpness has converged in the centre

View file

@ -156,8 +156,10 @@ class BaseCamera(lt.Thing):
def start_streaming( def start_streaming(
self, main_resolution: tuple[int, int], buffer_count: int self, main_resolution: tuple[int, int], buffer_count: int
) -> None: ) -> None:
"""Start (or stop and restart) the camera with the given resolution """Start (or stop and restart) the camera.
for the main stream, and buffer_count number of images in the buffer
:param main_resolution: the resolution to use for the main stream.
:param buffer_count: number of images in the stream buffer.
""" """
raise NotImplementedError( raise NotImplementedError(
"CameraThings must define their own start_streaming method" "CameraThings must define their own start_streaming method"
@ -342,12 +344,14 @@ class BaseCamera(lt.Thing):
metadata_getter: lt.deps.GetThingStates, metadata_getter: lt.deps.GetThingStates,
logger: lt.deps.InvocationLogger, logger: lt.deps.InvocationLogger,
) -> Image: ) -> Image:
"""Capture an image in memory and return it with metadata """Capture an image in memory and return it with metadata.
CaptureError raised if the capture fails for any reason
returns tuple with PIL Image, and dict of metadata.
This robust capturing method attempts to capture the image five times This robust capturing method attempts to capture the image five times
each time with a 5 second timeout set. each time with a 5 second timeout set.
:raises CaptureError: if the capture fails for any reason
:returns: tuple with PIL Image, and dictionary of metadata.
""" """
for capture_attempts in range(5): for capture_attempts in range(5):
try: try:

View file

@ -62,8 +62,9 @@ class PicameraStreamOutput(Output):
class SensorMode(BaseModel): class SensorMode(BaseModel):
"""A Pydantic model holding all the information about a specific """A Pydantic model holding all the information about a specific sensor mode.
sensor mode as reported by the PiCamera.
This data is as reported by the PiCamera2 module.
""" """
unpacked: str unpacked: str
@ -76,10 +77,11 @@ class SensorMode(BaseModel):
class SensorModeSelector(BaseModel): class SensorModeSelector(BaseModel):
"""A Pydantic model holding the two values needed to select a PiCamera """A Pydantic model holding the two values needed to select a PiCamera Sensor mode.
Sensor mode. The output size and the bit depth.
This is a Pydantic modell so that it can be saved to the disk. Theses values are the output size and the bit depth.
This is a Pydantic model so that it can be saved to disk.
""" """
output_size: tuple[int, int] output_size: tuple[int, int]

View file

@ -477,12 +477,16 @@ def set_static_geq(
tuning: dict, tuning: dict,
offset: int = 65535, offset: int = 65535,
) -> None: ) -> None:
"""Update the ``rpi.geq`` section of a camera tuning dict to always use green """Update the ``rpi.geq`` section of a camera tuning dict.
equalisation that averages the green pixels in the red and blue rows.
``tuning`` will be updated in-place to set the geq offest to the given value. :param tuning: the raspberry pi tuning file. This will be updated in-place to
The default 65535 is the maximum allowed value. This means set the geq offest to the given value.
the brightness will always be below the threshold where averaging is used. :param offset: The desired green equalisation offset. Default 65535. The default is
the maximum allowed value. This means the brightness will always be below the
threshold where averaging is used. This is default as we always need the green
equalisation to averages the green pixels in the red and blue rows due to the
chief ray angle compensation issue when the the stock lens is replaced by an
objective.
""" """
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq") geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
geq["offset"] = offset # max out offset to disable the adaptive green equalisation geq["offset"] = offset # max out offset to disable the adaptive green equalisation

View file

@ -311,8 +311,9 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _save_scan_inputs_json(self): def _save_scan_inputs_json(self):
"""Save scan inputs as a JSON file in the scan folder, to allow """Save scan inputs as a JSON file in the scan folder.
the user to review the settings used in the scan
This file allows the user to review the settings used in the scan.
""" """
# Should this be a method of the scan_data dataclass? # Should this be a method of the scan_data dataclass?
@ -335,11 +336,10 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _update_scan_data_json(self, scan_result: str): def _update_scan_data_json(self, scan_result: str):
"""Update scan data as a JSON file in the scan folder, with """Update scan data JSON file with data only known at the end of the scan.
data only known at the end of the scan.
Takes scan_result, a string that is either "success", Takes scan_result, a string that is either "success", "cancelled by user",
"cancelled by user", or the error that ended the scan. or the error that ended the scan.
""" """
# Should this be a method of the scan_data dataclass? # Should this be a method of the scan_data dataclass?
current_time = datetime.now().replace(microsecond=0) current_time = datetime.now().replace(microsecond=0)
@ -378,10 +378,11 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _run_scan(self): def _run_scan(self):
"""Prepare and run the main scan, handling the threads performing """Prepare and run the main scan, and perform final actions on completion.
stitching. Uses the result (or exception) from the scanning to
determine whether the scan should be stitched and the microscope The result (or exception) from the main scan loop determines whether the
should return to the starting x,y,z position scan should be stitched and whether the microscope should return to the
starting x,y,z position.
""" """
# Used to check if finally was reached via exception (except # Used to check if finally was reached via exception (except
# cancel by user) # cancel by user)
@ -765,8 +766,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _preview_stitch_start(self, overlap: float) -> None: def _preview_stitch_start(self, overlap: float) -> None:
"""Start stitching a preview of the scan in a background subprocess """Start stitching a preview of the scan in a background subprocess.
This uses popen and returns immediately This uses popen and returns immediately
@ -954,8 +954,9 @@ class SmartScanThing(lt.Thing):
self, self,
scan_name: str, scan_name: str,
): ):
"""Update the zip to include the files left until the end, then return the """Return zip after including any files left until the end.
zip file as a Blob
The zipfile is returned as a Blob.
""" """
zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True) zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True)
return ZipBlob.from_file(zip_fname) return ZipBlob.from_file(zip_fname)

View file

@ -43,8 +43,10 @@ class ErrorCapturingThread(Thread):
) )
def join(self, timeout=None): def join(self, timeout=None):
"""Join when the thread is complete. If the thread ended due to an unhandled exception, """Join when the thread is complete.
the exception will be raised when this method is called.
If the thread ended due to an unhandled exception, the exception will be raised
when this method is called.
""" """
super().join(timeout) super().join(timeout)
# If there is an error in the error buffer clear the buffer and raise it # If there is an error in the error buffer clear the buffer and raise it

View file

@ -183,9 +183,7 @@ def test_scan_sequence_and_listing():
def test_scan_name_non_sequential(): def test_scan_name_non_sequential():
"""Check created scans is the correct name if the directories """Check new scan has the correct name if the directories are not sequential"""
are not sequential
"""
_clear_scan_dir() _clear_scan_dir()
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0001")) os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0001"))
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0002")) os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0002"))

View file

@ -222,8 +222,10 @@ def test_mark_wrong_location():
def test_closest_focus_wth_large_numbers(): def test_closest_focus_wth_large_numbers():
"""The number of steps gets very large in reality runs some tests to check """Tests to check that everything works well with huge numbers of steps.
that everything works well with huge numbers of steps
The number of steps gets very large on the micorscope. But most of the tests
above use smaller numbers for clarity.
""" """
intial_position = (0, 0) intial_position = (0, 0)
# Set this up, but we won't use the settings # Set this up, but we won't use the settings
@ -247,11 +249,18 @@ def test_closest_focus_wth_large_numbers():
def test_example_smart_spiral(): def test_example_smart_spiral():
"""Test the smart spiral scan algorithm on the sample types listed """Test the smart spiral scan algorithm on the different sample types.
below and defined in scan_test_helpers.load_sample_points
Will fail if the locations or path between locations visited has changed The sample types:
for any of the samples listed
* ``regular``
* ``lobed``
* ``core"``
These are defined in scan_test_helpers.load_sample_points
This will fail if the locations or path between locations visited has changed
for any of the samples listed.
""" """
example_samples = [ example_samples = [
"regular", "regular",

View file

@ -63,8 +63,10 @@ def test_initial_properties(smart_scan_thing):
def test_inaccessible_scan_methods(smart_scan_thing): def test_inaccessible_scan_methods(smart_scan_thing):
"""The @_scan_running decorator should make some methods """Test that method with @_scan_running decorator is inaccessible.
inaccessible unless a scan is running
The @_scan_running decorator makes these functions inacessible unless
a scan is running.
""" """
with pytest.raises(ScanNotRunningError): with pytest.raises(ScanNotRunningError):
smart_scan_thing._run_scan() smart_scan_thing._run_scan()

View file

@ -46,7 +46,7 @@ def even_integers(min_value=0, max_value=1000):
extra_ims=even_integers(min_value=0, max_value=10), extra_ims=even_integers(min_value=0, max_value=10),
) )
def test_stack_params_validation(save_ims, extra_ims): def test_stack_params_validation(save_ims, extra_ims):
"""Tests specifically the validation on the image numbers """Test the validation of the image numbers for a stack.
save_ims is the number to save (must be odd and positive) save_ims is the number to save (must be odd and positive)
extra_ims is how many more images there are in min_images_to_test than extra_ims is how many more images there are in min_images_to_test than
@ -68,8 +68,9 @@ def test_stack_params_validation(save_ims, extra_ims):
extra_ims=even_integers(min_value=-10, max_value=-1), extra_ims=even_integers(min_value=-10, max_value=-1),
) )
def test_stack_params_not_enough_test_images(save_ims, extra_ims): def test_stack_params_not_enough_test_images(save_ims, extra_ims):
"""Set the extra_ims negative so that min_images_to_test is smaller """Test error is raised if min_images_to_test is smaller than images_to_save.
than images_to_save.
``extra_ims`` negative so that min_images_to_test is smaller than images_to_save.
For arguments see test_stack_params_validation For arguments see test_stack_params_validation
""" """

View file

@ -14,23 +14,23 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
class FakeSample: class FakeSample:
"""A fake sample to test scan algorithms. The sample is able to return """A fake sample to test scan algorithms.
whether a given position is sample, no image associated with the sample
The sample is able to return whether a given position is sample, there is no image
associated with the sample
""" """
def __init__(self, xy_points: list[tuple[int, int]]): def __init__(self, xy_points: list[tuple[int, int]]):
"""Create the sample from a spline interpolation around """Create the sample from a spline interpolation around the given points."""
the given points.
"""
self._sample_perimeter = interp_closed_path(xy_points, 500) self._sample_perimeter = interp_closed_path(xy_points, 500)
def is_sample(self, pos: tuple[int, int], im_size: tuple[int, int]) -> bool: def is_sample(self, pos: tuple[int, int], im_size: tuple[int, int]) -> bool:
"""Return whether an image at a given location with a given image size """Return True if an image at a given location is on the sample.
is on the sample
This doesn't check the entire image field as this is designed to be used The image size is specified to check if it overlaps the sample. It doesn't
where the fake sample is much larger than the image and has smooth edges check the entire image field as this is designed to be used where the fake
It just checks the 4 corners sample is much larger than the image and has smooth edges. It just checks the
4 corners.
""" """
img_corners = [ img_corners = [
(pos[0] + im_size[0], pos[1] + im_size[1]), (pos[0] + im_size[0], pos[1] + im_size[1]),
@ -77,8 +77,9 @@ def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Fi
def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPath: def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPath:
"""Given a lists of xy_points interpolate an n_point closed curve. This can be used """Interpolate an n_point closed curve from a lists of xy_points.
to create an arbitrary sample shape plan a scan.
This can be used to create an arbitrary sample shape for testing a scan planner.
Modified from: Modified from:
https://stackoverflow.com/questions/33962717/interpolating-a-closed-curve-using-scipy https://stackoverflow.com/questions/33962717/interpolating-a-closed-curve-using-scipy
@ -105,8 +106,9 @@ def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPa
def example_smart_spiral( def example_smart_spiral(
sample_name: str = "lobed", sample_name: str = "lobed",
) -> tuple[FakeSample, scan_planners.ScanPlanner]: ) -> tuple[FakeSample, scan_planners.ScanPlanner]:
"""Run an example scan and return the sample scanned and the planner object """Run an example scan.
after scan is complete
:returns: The sample scanned and the planner object after scan is complete.
""" """
xy_sample_points = load_sample_points(sample_name) xy_sample_points = load_sample_points(sample_name)
sample = FakeSample(xy_sample_points) sample = FakeSample(xy_sample_points)
@ -126,11 +128,11 @@ def example_smart_spiral(
def profile_and_save_plot_for_example_smart_spiral(): def profile_and_save_plot_for_example_smart_spiral():
"""Run the example scan and save a plot and the profile data """Run the example scan and save a plot and the profile data.
Also print the cumulative stats
Also print the cumulative stats.
This runs if you run this file directly This runs if you run this file directly.
""" """
import pstats import pstats
import cProfile import cProfile
@ -152,8 +154,9 @@ def profile_and_save_plot_for_example_smart_spiral():
def update_example_smart_spiral_pickle(sample_name: str): def update_example_smart_spiral_pickle(sample_name: str):
"""Pickle the ScanPlanner for the example_smart_spiral(), """Pickle the ScanPlanner for the example_smart_spiral().
this is done so the history can be compared by testing to check
This is done so the history can be compared by testing to check
the algorithm is unchanged. the algorithm is unchanged.
If the algorithm is purposefully changed then this will need to be If the algorithm is purposefully changed then this will need to be
@ -171,8 +174,9 @@ def update_example_smart_spiral_pickle(sample_name: str):
def get_expected_result_for_example_smart_spiral( def get_expected_result_for_example_smart_spiral(
sample_name: str, sample_name: str,
) -> scan_planners.ScanPlanner: ) -> scan_planners.ScanPlanner:
"""Return the expected ScanPlanner object for the example_smart_spiral(), """Return the expected ScanPlanner object for the example_smart_spiral().
this is pickled, so that it can be committed.
This is loaded from a pickle so that the object can be committed to the repo.
""" """
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl") pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")
with open(pkl_fname, "rb") as pkl_file_obj: with open(pkl_fname, "rb") as pkl_file_obj: