Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1e21143
chore: add rich dependency
holsam Aug 19, 2026
0c533d8
feat(frames): add print angle option and functions
holsam Aug 19, 2026
ce5ee07
feat(cli): add `--print-angles` option
holsam Aug 19, 2026
33f31d6
test(frames): add test for printing angle table
holsam Aug 19, 2026
895d74f
pr: merge holsam/print-angles-frames to holsam/v2.4.0-release
holsam Aug 19, 2026
62f37ea
feat(frames): resolve per series curtaining angle
holsam Aug 19, 2026
bf862d2
feat(cli): add `--anchor-tilts` option
holsam Aug 19, 2026
d9f4421
feat(frames): use nearest accepted neighbour as fb
holsam Aug 19, 2026
c5bcf55
test: update test suite
holsam Aug 20, 2026
5c431cf
pr: merge holsam/seeded-consensus to holsam/v2.4.0-release
holsam Aug 20, 2026
b8446cc
docs: update readme
holsam Aug 20, 2026
e6b1338
chore: add dev dependency pytest-cov
holsam Aug 20, 2026
be6c05b
chore: add pytest-cov options to pyproject.toml
holsam Aug 20, 2026
fb947a5
test: add fixture funcs as pytest fixture
holsam Aug 20, 2026
f85dc54
fix(io): wrap mrc file reader in error handler
holsam Aug 20, 2026
0747e69
fix: update angle resolution to skip bad mrc files
holsam Aug 20, 2026
a206771
test: update tests to use fixtures directly
holsam Aug 20, 2026
9272d52
test: add `--version` and frames unit tests
holsam Aug 20, 2026
3ddbdf3
test: add dc_protect_frac preservation unit test
holsam Aug 20, 2026
6ad0cf5
test: add unit tests for frames processing
holsam Aug 20, 2026
6d10951
test: add unit test for nameOutputFile
holsam Aug 20, 2026
41475f3
test: add invalid field and unparseable tilt cases
holsam Aug 20, 2026
45e7240
test: add additional unit tests
holsam Aug 20, 2026
a8d5b85
chore(release): update version to v2.4.0
holsam Aug 20, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ dist/
wheels/
*.egg-info

# Pytest files
.coverage
.pytest_cache

# Virtual environments
.venv
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pylisc --help
```sh
pylisc stack [OPTIONS] INPUT_MRC [OUTPUT_MRC]
```
`OUTPUT_MRC` is optional. If omitted, it defaults to `INPUT_MRC` with a `_PyLisC_{mode}` suffix, saved to the same directory as `INPUT_MRC`. If that file already exists, a numeric suffix is appended instead of overwriting it.
`OUTPUT_MRC` is optional. If omitted, it defaults to `INPUT_MRC` with a `_PyLisC_{mode}` suffix, saved to the same directory as `INPUT_MRC`. If that file already exists, a numeric suffix is appended instead of overwriting it (pass `--force` to overwrite in place instead).

#### Options

Expand All @@ -57,7 +57,7 @@ Option | Default | Description
--|--|--
`-m`, `--mode` | `angular` | Destriping approach: `angular` (recommended) or `linear` (deprecated). See [Destriping mode](#destriping-mode) below.
`--angle` | *(auto-estimated)* | Curtaining orientation, degrees from horizontal. Omit to estimate automatically from the tilt series' central frame[^estimation]; pass a value to override. A diagnostic plot is saved alongside the output when auto-estimated[^diagnosticplot].
`--reference-frame` | `0` | Stack index used for angle estimation and the destriping preview. See [Choosing a reference frame](#choosing-a-reference-frame) for more information.
`--reference-frame` | *(mid-stack index)* | Stack index used for angle estimation and the destriping preview. See [Choosing a reference frame](#choosing-a-reference-frame) for more information.
`--angular-width` | `8.0` | Angular width of the destriping notch, in degrees. Only used when `--mode angular`. Narrower keeps more real structure sharing a nearby angle to the curtains, at the cost of weaker curtain removal.
`--notch-fraction` | `0.03` | Width of the destriping notch, as a fraction of image width. Narrower removes less real signal running parallel to the curtains, but leaves more curtaining behind. Only used when `--mode linear`, which is deprecated.
`--protect-fraction` | `0.01` | Fraction of image width around the zero-frequency (DC) origin exempted from destriping. See [Destriping mode](#destriping-mode) below for why this exists and its trade-off. Only used when `--mode linear`, which is deprecated.
Expand All @@ -66,7 +66,16 @@ Option | Default | Description
Option | Default | Description
--|--|--
`--output-dir` | *(required for directory input)* | Output directory for batch mode, mirroring the input directory's structure.
`--angle-outlier-threshold` | `5.0` | Warn if an individual series' own angle estimate differs from the batch consensus by more than this many degrees. In `frames` mode, a tilt beyond this threshold also has its angle replaced with its nearest reliable tilt's angle, see [per-tilt curtain angle](#per-tilt-curtain-angle) for details.
`--angle-outlier-threshold` | `5.0` | Warn if an individual series' own angle estimate differs from the batch consensus by more than this many degrees.
`--workers` | `0` (all CPUs) | Number of parallel processes to use in batch mode.

##### Other options
Option | Default | Description
--|--|--
`--force` | - | Overwrite existing output files instead of erroring or auto-numbering.
`--dry-run` | - | Print what would be processed/written, including the resolved angle and pixel size, without writing any output.
`-v`, `--verbose` | - | Increase logging verbosity (repeat for more, e.g. `-vv`).

#### Example
```sh
# Run PyLisC, estimating the curtaining angle automatically
Expand All @@ -89,7 +98,7 @@ Before committing to a full run, different strength values can be previewed agai
pylisc stack --preview-strengths 3,5,8,12,20 tilt_series.mrc
```

This saves `destripe_strength_preview.tiff`, a side-by-side montage labelled with each value, and exits without processing the rest of the stack. Uses `--reference-frame` (see [below](#choosing-a-reference-frame)) as the preview frame.
This saves `destripe_strength_preview.tiff`, a side-by-side montage labelled with each value, and exits without processing the rest of the stack. Uses `--reference-frame` (see [below](#choosing-a-reference-frame)) as the preview frame. Only applies in single-file mode; in batch mode it is ignored with a warning.

### Batch mode

Expand Down Expand Up @@ -123,7 +132,12 @@ pylisc frames [OPTIONS] --output-dir OUTPUT_DIR --filename-template TEMPLATE INP
This command is aimed at destriping tilt images that exist as individual 2D MRC frames, i.e. not yet assembled/aligned into a stack. `INPUT_DIR` is not recursed into; every `*.mrc` directly inside it (excluding PyLisC's own `_PyLisC_` output) is treated as one tilt image. `--output-dir` is required, and mirrors the input's flat structure: each frame is written back out individually with a `_PyLisC_{mode}` suffix, same as `--output-dir` does for [batch mode](#batch-mode).

#### Options
`pylisc frames` uses many of the same options as `pylisc stack`, see [above](#options) or run `pylisc frames -h` for further information.
`pylisc frames` uses many of the same options as `pylisc stack`, see [above](#options) or run `pylisc frames -h` for further information. Frames mode does not take `--reference-frame` or `--preview-strengths`, but adds:

Option | Default | Description
--|--|--
`--anchor-tilts` | `5` | Number of tilt buckets nearest each series' median tilt used to seed its consensus walk. See [Per-tilt curtain angle](#per-tilt-curtain-angle) below.
`--print-angles` | off | Print a diagnostic table of per-file and per-bucket angle estimation, and write it to CSV files in `--output-dir`.

#### Filename template
Since a flat directory has no per-series subdirectory to group frames by, PyLisC needs to know which filename field is the tilt angle. This is given as a template describing the delimited filename fields, with `{}` for fields to ignore and `{tilt}` (required) for the tilt angle field, e.g. for `Position_012_003_-30.00_20240115_1_Fractions_motion_corrected.mrc`:
Expand All @@ -143,11 +157,11 @@ Field boundaries default to underscore only. `--filename-delimiters` sets which
#### Per-tilt curtain angle
Curtaining orientation drifts slightly with tilt angle, so unless `--angle` is given explicitly, frames mode does **not** use one consensus angle for the whole directory. Instead:
1. Every frame's own angle is estimated.
2. Frames are grouped by tilt angle, rounded to the nearest whole degree (so e.g. two positions' `-30.00°` and `-29.98°` tilts fall in the same group).
3. Each frame's confidence ratio is clipped to a per-run cap before use, so a single sharp FFT peak can't dominate its group's consensus or the overall weighting below. The cap is the confidence distribution's median plus 3x its (scaled) median absolute deviation, which stays robust even with few frames (falling back to 5x the median when every value is identical, i.e. zero deviation).
4. Each group's estimates are combined into a per-tilt consensus (same confidence-weighted circular mean as [batch mode](#shared-curtain-angle)), using the clipped confidences, which is the angle applied to every frame in that group.
5. All per-tilt consensus angles are then combined into an overall consensus, weighted both by the group's typical (median) confidence and by `cos(tilt)` (sample thickness grows ~1/cos(tilt) so high tilt angles are less reliable), so they count for less than well-sampled low-tilt groups rather than skewing the overall consensus by an equal vote.
6. Any per-tilt consensus that still deviates from the overall consensus by more than `--angle-outlier-threshold` is treated as unreliable, and will be destriped at the consensus angle of its nearest reliable tilt (by tilt-angle distance) instead, logging a warning naming both tilts. If every tilt ends up flagged, PyLisC falls back to the overall consensus for all of them.
2. Each frame's confidence ratio is clipped to a per-run cap before use, so a single sharp FFT peak can't dominate its bucket's consensus. The cap is the confidence distribution's median plus 3x its (scaled) median absolute deviation, falling back to 5x the median when every value is identical, (i.e. zero deviation).
3. Frames are grouped by tilt angle, rounded to the nearest whole degree (so e.g. two positions' `-30.00°` and `-29.98°` tilts fall in the same bucket), and each bucket's estimates are combined into a per-tilt consensus (the same confidence-weighted circular mean as [batch mode](#shared-curtain-angle)), using the clipped confidences.
4. Tilt buckets are clustered into separate acquisition series (by matching frame count per bucket, then splitting wherever the tilt spacing breaks step), so a directory holding more than one tilt series is handled independently per series rather than as one pool.
5. Within each series, the `--anchor-tilts` buckets around the series' median tilt are used as a trusted seed. PyLisC then walks outward from this seed, and compares each tilt bucket's own consensus to the nearest already-resolved bucket. A tilt bucket's consensus angle is accepted (becoming the new "nearest resolved" point) if it's within `--angle-outlier-threshold` degrees of it, otherwise it is replaced with that nearest resolved angle and logged as a warning naming both tilts. This approach prevents buckets with incorrect angles from throwing off subsequent tilts, while allowing for slight angle drift over a tilt series.
6. Pass `--print-angles` to see the full per-file and per-bucket breakdown (angle, confidence, resolved angle, seed/accepted/rejected status) as tables and CSV files written to `--output-dir`.

#### Pixel size
Individual frame MRCs frequently lack a reliable pixel size in their header, so frames mode does not fall back to it. Pixel size is only needed for the optional high-pass filter — if `--apply-filter` is set, `--pixel-size` must be given explicitly, or PyLisC exits with an error.
Expand Down Expand Up @@ -184,7 +198,7 @@ The reference frame should be the tilt with the least foreshortening and the bes
- **Dose-symmetric schemes** (0° acquired first, then alternating ±): use `--reference-frame 0`.
- **Continuous sweeps** (most-negative tilt acquired first): 0° sits in the middle of the stack, so use roughly `--reference-frame <n//2>` for an n-tilt series.

By default, the reference frame is taken as the middle of the stack, under the assumption that this will be correct for continous sweeps and workable for dose-symmetric schemes (vs using the first frame which would be correct for dose-symmetric but an extreme tilt angle for continous).
By default, the reference frame is taken as the middle of the stack, under the assumption that this will be correct for continous sweeps and workable for dose-symmetric schemes (vs using the first frame which would be correct for dose-symmetric but an extreme tilt angle for continuous). An out-of-range `--reference-frame` falls back to this same default, with a warning.

### Limitations
- **Directional filering:** Any real structure running parallel to the curtains shares the same Fourier orientation and is attenuated along with them, in both destriping modes. A narrower `--angular-width` (or `--notch-fraction` in linear mode) limits this but cannot eliminate it where curtains and genuine structure share an angle. Some loss of signal is likely to be observed for these structures.
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pylisc"
version = "2.3.0"
version = "2.4.0"
description = "Python implementation of LisC algorithm"
readme = "README.md"
authors = [
Expand All @@ -12,6 +12,7 @@ dependencies = [
"matplotlib>=3.11.0",
"mrcfile>=1.5.4",
"numpy>=2.5.1",
"rich>=15.0.0",
"scipy>=1.18.0",
"tifffile>=2026.7.14",
"typer>=0.27.0",
Expand All @@ -27,4 +28,8 @@ build-backend = "uv_build"
[dependency-groups]
dev = [
"pytest>=9.1.1",
"pytest-cov>=7.1.0",
]

[tool.pytest.ini_options]
addopts = "--cov --cov-report=term-missing"
12 changes: 12 additions & 0 deletions src/pylisc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ def version_callback(value: bool | None) -> None:
int,
typer.Option('--workers', help='Number of parallel processes to use in batch mode (0: all CPUs).', min=0, rich_help_panel='Batch options'),
]
PrintAnglesOpt = Annotated[
bool,
typer.Option('--print-angles', help='Print a diagnostic table of per-file and per-bucket angle estimation (frames mode only).', rich_help_panel='Batch options', show_default=False),
]
AnchorTiltsOpt = Annotated[
int,
typer.Option('--anchor-tilts', help='Number of tilt buckets nearest each series\' median tilt used to seed its consensus walk (frames mode only).', rich_help_panel='Batch options', min=1),
]

# Define callback for pylisc (to allow version option)
@pylisc.callback()
Expand Down Expand Up @@ -215,6 +223,8 @@ def frames(
notch_frac: NotchFracOpt = 0.03,
dc_protect_frac: DcProtectFracOpt = 0.01,
angle_outlier_threshold: AngleOutlierThresholdOpt = 5.0,
anchor_tilts: AnchorTiltsOpt = 5,
print_angles: PrintAnglesOpt = False,
version: VersionOpt = None,
workers: WorkersOpt = 0,
):
Expand Down Expand Up @@ -247,9 +257,11 @@ def frames(
notch_frac=notch_frac,
dc_protect_frac=dc_protect_frac,
angle_outlier_threshold=angle_outlier_threshold,
anchor_tilts=anchor_tilts,
force=force,
dry_run=dry_run,
workers=workers,
print_angles=print_angles,
)
logger.info('pylisc completed')
raise typer.Exit()
Loading