Contributing Guidelines

General Overview

We welcome contributions in the form of bug reports, bug fixes, new features and documentation. Some best practices for contributing code are outlined below. If you are considering refactoring the code, please reach out to us prior to starting this process.


Project Philosophy

navigate is designed with the following principles in mind:

  • Prioritize standard library imports for maximum stability, and minimize external dependencies.

  • Abstraction layer to drive different camera types, etc.

  • Plugin architecture for extensibility.

  • Maximize productivity for biological users through robust graphical user interface-based workflows.

  • Performant and responsive.

  • Brutally obvious, well-documented, clean code organized in an industry standard Model-View-Controller architecture.

We ask that all contributions adhere to these principles.


General Principles

  • We use a model-view-controller architecture. New functionality should keep this strong separation. More information can be found in the software architecture section.

  • Please do not create new configuration variables unless absolutely necessary, especially in the configuration.yaml and experiment.yaml files. A new variable is necessary only if no variable stores similar information or there is no way to use the most similar variable without disrupting another part of the code base.

  • We are happy to discuss code refactors for improved clarity and speed. However, please do not modify something that is already working without discussing this with the software team in advance.

  • All code that modifies microscope control behavior must be reviewed and tested on a live system prior to merging into the develop branch.

  • Scientific Units - Please express quantities in the following units when they are in the standard model/view/controller code. Deviations from this can occur where it is necessary to pass a different unit to a piece of hardware.

    • Time - Milliseconds

    • Distance - Micrometers

    • Voltage - Volts

    • Rotation - Degrees


Getting Started

  1. Fork and Clone: Fork the repository and clone it locally

  2. Set Up Environment: pip install -e .[dev] to install in development mode

  3. Install Pre-commit Hooks: pre-commit install to set up linting hooks

  4. Run Tests: Ensure pytest passes before making changes


Pull Request Process

  1. Create a branch from develop with a descriptive name

  2. Make your changes following the guidelines in this document

  3. Add tests for new functionality

  4. Ensure all tests pass and linting is clean

  5. Update documentation as needed

  6. Submit PR to the develop branch with clear description of changes


Coding Style

Naming Conventions

We follow the PEP8 code style guide. All class names are written in CamelCase and all variable names are lowercase_and_separated_by_underscores.

Type Hints

Type hints are used throughout the code base. Please add type hints to any new methods or functions you create. If you are unsure how to do this, please see PEP 484 for more information.

Numpydoc

We use Numpydoc style docstrings throughout the code base. Please use this style for any new methods or functions you create.

Sphinx

We use Sphinx to generate documentation from documented methods, attributes, and classes. Please document all new methods, attributes, and classes using a Sphinx compatible version of Numpydoc.

Linters

We use Ruff to enforce consistent code formatting. Please run Ruff on your code before making a pull request. Ideally, these actions should be integrated as part of a pre-commit hook (see below).

Pre-commit Hooks

We use pre-commit hooks to enforce consistent code formatting and automate some of the code review process. In some rare cases, the linter may complain about a line of code that is actually fine. For example, in the example code below, Ruff linter complains that the start_stage class is imported but not used. However, it is actually used in as part of an exec statement.

from navigate.model.device_startup_functions import start_stage
device_name = stage
exec(f"self.{device_name} = start_{device_name}(name, device_connection, configuration, i, is_synthetic)")

To avoid this error, add a # noqa comment to the end of the line to tell Ruff to ignore the error.

from navigate.model.device_startup_functions import start_stage  # noqa

Unit Tests

Each line of code is unit tested to ensure it behaves appropriately and alert future coders to modifications that break expected functionality. Guidelines for writing good unit tests can be found here and over here, or in examples of unit tests in this repository’s test folder. We use the pytest library to evaluate unit tests. Please check that unit tests pass on your machine before making a pull request.

Dictionary Parsing

The configuration file is loaded as a large dictionary object, and it is easy to create small errors in the dictionary that can crash the program. To avoid this, when getting properties from the configuration dictionary, it is best to use the .get() command, which provides you with the opportunity to also have a default value should the key provided not be found. For example,

# Galvo Waveform Information
self.galvo_waveform = self.device_config.get("waveform", "sawtooth")

Here, we try to retrieve the waveform key from a the self.device_config dictionary. In the case that this key is not available, it then by default returns sawtooth. If however the waveform key is found, it will provide the value associated with it.


Communicating with Hardware

Threads and Blocking

In handling hardware devices, such as Sutter’s MP-285A stage, using threads can introduce complexities, especially when simultaneous read and write operations occur over a shared resource like a serial line. An encountered issue demonstrated the challenges when two different threads attempted to write to and read from the same serial port simultaneously. This action led to data corruption due to interleaving of read/write calls that require precise handshaking, characteristic of the MP-285A’s communication protocol. The solution involved implementing a blocking mechanism using threading.Event() to ensure that operations on the serial port do not overlap, showcasing the difficulties of multithreading sequential processes. To mitigate such issues, a design where each hardware device operates within its own dedicated thread is advisable. This approach simplifies the management of device communications by enforcing sequential execution, eliminating the need to handle complex concurrency issues inherent in multithreading environments. This strategy ensures robust and error-free interaction with hardware devices.

Dedicated Device Interfaces

navigate implements a robust hardware abstraction layer through dedicated device interfaces. When integrating new hardware devices:

  • Each hardware device type (cameras, stages, etc.) has its own dedicated interface that must be implemented

  • All hardware classes inherit from a base class specific to the device type

  • Base classes include AbstractMethods that define the required interface for any derived hardware class

  • These abstract methods clearly communicate which functions must be implemented for any new hardware

  • Failure to override these abstract methods in derived classes will result in runtime errors

This architecture ensures consistency across different hardware implementations while providing clear guidance for developers adding support for new devices. When adding support for a new hardware device, first identify the appropriate base class and ensure you implement all required abstract methods.