Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .flake8

This file was deleted.

25 changes: 25 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,31 @@ concurrency:
cancel-in-progress: true

jobs:
lint:
name: lint (black + flake8)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install lint tools
# Pin Black to the version the pre-commit hook / local devs run so CI
# and pre-commit agree. flake8 reads [tool.flake8] via Flake8-pyproject.
run: |
python -m pip install --upgrade pip
pip install "black==25.9.0" flake8 Flake8-pyproject

- name: black --check
run: black --check .

- name: flake8
run: flake8 .

unittest:
name: unittest on Windows (Python ${{ matrix.python-version }})
runs-on: windows-latest
Expand Down
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ PycroFlow.egg-info/*
*.egg-info/
build/
dist/
# Generated by setuptools-scm at build/install time (version from git tag).
PycroFlow/_version.py

# Test artifacts
.pytest_cache/
Expand All @@ -19,6 +21,7 @@ htmlcov/
# Local test data (fixtures live under PycroFlow/tests/fixtures/)
PycroFlow/TestData/*

# Claude Code (project instructions + local settings; kept out of version control)
CLAUDE.md
# Claude Code: CLAUDE.md (shared project instructions) IS committed; keep
# local settings and personal notes out of version control.
.claude/
CLAUDE.local.md
44 changes: 44 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Applies to every hook below. Keep the vendored upstream PyHamiltonPSD
# snapshot pristine, and leave the regression snapshot fixtures byte-exact
# (they are written without a trailing newline by the snapshot regenerator and
# compared via json.loads, so the end-of-file-fixer must not touch them).
exclude: >
(?x)^(
PycroFlow/pyHamilton/pyHamiltonPSD_packagefiles/
|PycroFlow/tests/fixtures/snapshots/
)

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
# Use the maintained black mirror, pinned to the version the team runs
# locally so pre-commit and a manual `black` agree (keep in sync via
# `pre-commit autoupdate`). black reads line-length from [tool.black].
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.9.0
hooks:
- id: black
# flake8 reads its config from pyproject.toml [tool.flake8] via the
# Flake8-pyproject plugin (no separate .flake8 file). The exclude below
# mirrors [tool.flake8].extend-exclude; it is repeated here because
# pre-commit passes an explicit file list that flake8's own directory-walk
# excludes do not filter (the generated _version.py, the pyHamilton driver's
# star-import idiom, and the throwaway snippets/scripts/example scripts).
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [Flake8-pyproject]
exclude: >
(?x)^(
PycroFlow/_version\.py
|PycroFlow/pyHamilton/
|snippets/
|scripts/
|example_experiment/
)
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Versioning now derives from the git tag via `setuptools-scm` (writes
`PycroFlow/_version.py`); the manual `version` string in `pyproject.toml`
is gone. `PycroFlow.__version__` reads the generated module with a fallback.
- Consolidated lint config into `pyproject.toml`: added `[tool.black]`
(line-length 79, `target-version = ["py310"]`) and `[tool.flake8]`
(`extend-ignore = E203,E501,W503` — Black owns line length), replacing the
standalone `.flake8`.

### Added

- Per-subsystem selection: an `enabled` flag on the fluid / img / illu
sections of an experiment design lets a subsystem be deselected. The
builder omits deselected subsystems from the compiled Run Sequence, prunes
cross-subsystem `wait for signal` entries that targeted a dropped
subsystem, and raises if nothing is selected; the orchestrator only wires
hardware for subsystems present in the protocol.
- Shared `.pre-commit-config.yaml` (pre-commit-hooks + Black + flake8 via
Flake8-pyproject), matching the rest of the DNA-PAINT stack.
- `black --check` and `flake8` lint job in CI.
- This changelog.

### Removed

- Legacy `setup.py` shim (`pyproject.toml` is the canonical build config).
- Empty `CHANGELOG.txt` (superseded by this `CHANGELOG.md`).

## [0.1.0]

Initial tagged release. PycroFlow coordinates microscopy image acquisition,
Hamilton fluid handling, and monet illumination control for automated
DNA-PAINT experiments (Exchange-PAINT, MERPAINT, Z-PAINT, SPH-RESI), with a
CLI (`pycroflow`) and a PyQt6 GUI (`pycroflow-gui`) over a shared service layer.
Empty file removed CHANGELOG.txt
Empty file.
141 changes: 141 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

28 changes: 20 additions & 8 deletions PycroFlow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
a protocol in a startup script) does not flood the terminal. Until
``setup_logging`` installs the real sinks, records simply go nowhere.
"""

from loguru import logger
import os
import sys

# Version comes from the git tag via setuptools-scm, which writes the
# resolved value into the generated ``_version.py`` at build/install time.
# Fall back to importlib.metadata (installed dist) and finally a sentinel so
# importing from an uninstalled source tree without a build never crashes.
try:
from importlib.metadata import PackageNotFoundError, version
from ._version import version as __version__
except ImportError: # no generated file (uninstalled source tree)
try:
from importlib.metadata import PackageNotFoundError, version

__version__ = version("PycroFlow")
except (ImportError, PackageNotFoundError): # not installed (e.g. source tree)
__version__ = "0.0.0"
__version__ = version("PycroFlow")
except (ImportError, PackageNotFoundError):
__version__ = "0.0.0"


# loguru auto-installs a DEBUG->stderr handler (id 0) on import. Drop it so
Expand All @@ -43,7 +51,7 @@

def log_filter(record):
"""Exclude subpackage logs (pyHamilton, monet) from the main log file."""
subpackages = ['pyHamilton', 'monet']
subpackages = ["pyHamilton", "monet"]
if any(sp in record["name"] for sp in subpackages):
return False
return True
Expand All @@ -59,7 +67,7 @@ def logging_configured():
return _LOGGING_CONFIGURED


def clean_old_logs(prefix='pycroflow.log', directory='.'):
def clean_old_logs(prefix="pycroflow.log", directory="."):
"""Delete rotated log files matching ``prefix`` in ``directory``.

Previously called ``rem_old_logfiles`` and run at import time, which
Expand All @@ -78,8 +86,12 @@ def clean_old_logs(prefix='pycroflow.log', directory='.'):
pass


def setup_logging(logfile='pycroflow.log', clean_old=False,
stderr_level='ERROR', hamilton_logfile='hamilton.log'):
def setup_logging(
logfile="pycroflow.log",
clean_old=False,
stderr_level="ERROR",
hamilton_logfile="hamilton.log",
):
"""Configure loguru sinks for PycroFlow.

Three sinks are installed:
Expand Down
68 changes: 35 additions & 33 deletions PycroFlow/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@
The package data is included via ``[tool.setuptools.package-data]`` in
``pyproject.toml``.
"""

import copy
from pathlib import Path

import yaml


_CONFIG_DIR = Path(__file__).resolve().parent
_SETUP_DIR = _CONFIG_DIR / 'setups'
_SETUP_DIR = _CONFIG_DIR / "setups"


def _resolve(path_or_name, suffix, base=None):
"""Accept either a bare name ('default') or a path; return a Path."""
p = Path(path_or_name)
if p.suffix == '':
p = (base or _CONFIG_DIR) / f'{path_or_name}{suffix}'
if p.suffix == "":
p = (base or _CONFIG_DIR) / f"{path_or_name}{suffix}"
return p


Expand All @@ -35,22 +35,22 @@ def _records_to_tubing(records):
"""
result = {}
for record in records:
result[(record['from'], record['to'])] = record['volume']
result[(record["from"], record["to"])] = record["volume"]
return result


def load_legacy_system(name='legacy_system'):
def load_legacy_system(name="legacy_system"):
"""Load a legacy system config YAML and return the parsed dict.

``name`` may be either a basename (e.g. ``'legacy_system'``) found in
:mod:`PycroFlow.configs`, or an absolute / relative path to a YAML file.
"""
path = _resolve(name, '.yaml')
path = _resolve(name, ".yaml")
with open(path) as f:
return yaml.safe_load(f)


def load_legacy_tubing(name='legacy_tubing'):
def load_legacy_tubing(name="legacy_tubing"):
"""Load a legacy tubing config and convert list-of-records to
tuple-keyed dict, matching the original in-source dict shape.

Expand All @@ -62,14 +62,15 @@ def load_legacy_tubing(name='legacy_tubing'):

which round-trips to ``{('R21', 'pump_a'): 365, ...}``.
"""
path = _resolve(name, '.yaml')
path = _resolve(name, ".yaml")
with open(path) as f:
records = yaml.safe_load(f)
return _records_to_tubing(records)


# --- Per-microscope setup (hardware) configs -----------------------------


def list_setups():
"""Return the names of the available setup configs.

Expand All @@ -81,7 +82,7 @@ def list_setups():
"""
if not _SETUP_DIR.is_dir():
return []
return sorted(p.stem for p in _SETUP_DIR.glob('*.yaml'))
return sorted(p.stem for p in _SETUP_DIR.glob("*.yaml"))


def load_setup(name):
Expand All @@ -101,11 +102,11 @@ def load_setup(name):
dict
The parsed setup with ``tubing`` converted to a tuple-keyed dict.
"""
path = _resolve(name, '.yaml', base=_SETUP_DIR)
path = _resolve(name, ".yaml", base=_SETUP_DIR)
with open(path) as f:
setup = yaml.safe_load(f)
if isinstance(setup.get('tubing'), list):
setup['tubing'] = _records_to_tubing(setup['tubing'])
if isinstance(setup.get("tubing"), list):
setup["tubing"] = _records_to_tubing(setup["tubing"])
return setup


Expand Down Expand Up @@ -135,14 +136,14 @@ def assemble_hamilton_config(setup, fluid_settings):
``(hamilton_config, tubing_config)`` ready for
``LegacyArchitecture(hamilton_config, tubing_config)``.
"""
hamilton = copy.deepcopy(setup['hamilton'])
manifold = hamilton.pop('reservoir_a_manifold', [])
by_id = {entry['id']: entry for entry in manifold}
hamilton = copy.deepcopy(setup["hamilton"])
manifold = hamilton.pop("reservoir_a_manifold", [])
by_id = {entry["id"]: entry for entry in manifold}

special_names = dict(fluid_settings.get('special_names', {}))
cleaning = fluid_settings.get('cleaning_reservoirs', []) or []
special_names = dict(fluid_settings.get("special_names", {}))
cleaning = fluid_settings.get("cleaning_reservoirs", []) or []

used_ids = list(fluid_settings.get('reservoir_names', {}).keys())
used_ids = list(fluid_settings.get("reservoir_names", {}).keys())
for res in cleaning:
if isinstance(res, int):
rid = res
Expand All @@ -151,7 +152,8 @@ def assemble_hamilton_config(setup, fluid_settings):
if rid is None:
raise KeyError(
"Cleaning reservoir {!r} is neither an int id nor a "
"name in special_names {}".format(res, special_names))
"name in special_names {}".format(res, special_names)
)
if rid not in used_ids:
used_ids.append(rid)

Expand All @@ -160,14 +162,14 @@ def assemble_hamilton_config(setup, fluid_settings):
if rid not in by_id:
raise KeyError(
"Reservoir id {!r} is not wired in setup "
"{!r}'s reservoir_a_manifold".format(
rid, setup.get('setup')))
"{!r}'s reservoir_a_manifold".format(rid, setup.get("setup"))
)
reservoir_a.append(by_id[rid])

hamilton['reservoir_a'] = reservoir_a
hamilton['special_names'] = special_names
hamilton['cleaning_reservoirs'] = cleaning
return hamilton, setup.get('tubing', {})
hamilton["reservoir_a"] = reservoir_a
hamilton["special_names"] = special_names
hamilton["cleaning_reservoirs"] = cleaning
return hamilton, setup.get("tubing", {})


def assemble_imaging_config(setup, design):
Expand All @@ -188,11 +190,11 @@ def assemble_imaging_config(setup, design):
dict
Config for :class:`PycroFlow.imaging.ImagingSystem`.
"""
cfg = copy.deepcopy(setup.get('imaging', {}))
cfg.setdefault('save_dir', design.get('save_dir', '.'))
cfg['base_name'] = design.get('base_name', 'experiment')
img = design.get('img', {})
settings = img.get('settings', {}) if isinstance(img, dict) else {}
if 'use_positions' in settings:
cfg['use_positions'] = settings['use_positions']
cfg = copy.deepcopy(setup.get("imaging", {}))
cfg.setdefault("save_dir", design.get("save_dir", "."))
cfg["base_name"] = design.get("base_name", "experiment")
img = design.get("img", {})
settings = img.get("settings", {}) if isinstance(img, dict) else {}
if "use_positions" in settings:
cfg["use_positions"] = settings["use_positions"]
return cfg
Loading
Loading