Metadata-Version: 2.4
Name: fritts
Version: 0.3.0
Summary: Open-source desktop tool for tree-ring cross-dating, measurement, and master chronology building.
Author: Mark
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyQt6<7,>=6.6
Requires-Dist: pyqtgraph<0.14,>=0.13
Requires-Dist: pandas<3,>=2.1
Requires-Dist: numpy<3,>=1.26
Requires-Dist: scipy<2,>=1.12
Requires-Dist: lxml<7,>=5.0
Requires-Dist: tifffile>=2024.1
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-qt>=4.3; extra == "dev"
Requires-Dist: pytest-mock>=3.10; extra == "dev"
Requires-Dist: hypothesis>=6.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# Fritts — Dendrochronology Analysis Platform

![Fritts Screenshot](assets/fritts.png)

![Fritts hero illustration — dendrochronology-themed artwork](assets/fritts_hero.png)

_An open-source desktop application for tree-ring cross-dating, measurement, and master chronology building. Built with Python, PyQt6, and PyQtGraph. Named in honor of **Harold C. Fritts** (1930–2024), author of *Tree Rings and Climate* and a pioneer of dendroclimatology._

[![CI](https://img.shields.io/github/actions/workflow/status/mabo-du/fritts/ci.yml?branch=master&label=CI&logo=github)](https://github.com/mabo-du/fritts/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/fritts?logo=pypi)](https://pypi.org/project/fritts/)
[![Python version](https://img.shields.io/pypi/pyversions/fritts?logo=python)](https://pypi.org/project/fritts/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**Repository**: [GitHub](https://github.com/mabo-du/fritts) · [GitLab](https://gitlab.com/mabodu/fritts)
**Package**: [`fritts` on PyPI](https://pypi.org/project/fritts/)

---

## Table of Contents

1. [Why Fritts?](#why-fritts)
2. [Features](#features)
3. [Quick Start](#quick-start)
4. [Installation](#installation)
5. [Supported Formats](#supported-formats)
6. [Dependencies](#dependencies)
7. [Usage](#usage)
8. [Cross-Dating Methods](#cross-dating-methods)
9. [COFECHA-Style Quality Control](#cofecha-style-quality-control)
10. [Chronology Building](#chronology-building)
11. [Image Measurement](#image-measurement)
12. [Session Workspace](#session-workspace)
13. [ITRDB Integration](#itrdb-integration)
14. [Keyboard Shortcuts](#keyboard-shortcuts)
15. [Sample Data](#sample-data)
16. [Project Structure](#project-structure)
17. [Development](#development)
18. [User Guide](#user-guide)
19. [Target Users](#target-users)
20. [Tech Stack](#tech-stack)
21. [Known Issues](#known-issues)
22. [Limitations](#limitations)
23. [Contributing](#contributing)
24. [License](#license)

---

## Why Fritts?

Existing dendrochronology software is often dated, proprietary, Windows-only, or splits critical workflows across multiple applications. **Fritts** unifies format parsing, interactive visual plotting, statistical cross-dating, and chronology building into a single, modern, cross-platform interface.

Key differentiators:

- **All-in-one** — Import, visualize, cross-date, detrend, build chronologies, and export in a single application.
- **Modern GUI** — PyQtGraph-powered canvas with GPU-accelerated rendering, smooth zoom/pan, and multi-series overlay.
- **Standards-compliant** — Full TSAP-Win Heidelberg `.fh` spec (compatible with `dplR::read.fh`), Tucson decadal read/write, TRiDaS read/write.
- **COFECHA-compatible QC** — Sliding-window correlation with AR prewhitening and Spearman significance (matching `dplR` defaults). Runs on a background thread so the UI never freezes.
- **Standard EPS** — Wigley/Briffa (1984) Expressed Population Signal with per-tree metadata and overlap-weighted between-tree R-bar.
- **ITRDB integration** — Search and download data directly from the NOAA International Tree-Ring Data Bank (SSRF-hardened, HTTPS-only client).
- **QThread-accelerated analyses** — Cross-date, detrend, build chronology, and QC all run off the GUI thread with progress dialogs.
- **Full undo/redo** — Every UI mutation (including `AddSeriesCommand` for imports) goes through the CommandStack with a 200-step history.
- **Cross-platform** — Linux, macOS, and Windows with optional standalone binaries.

## Features

- **Multi-format import/export** — Tucson (`.rwl`, `.tuc`, `.crn`), Heidelberg (`.fh`), TRiDaS (`.xml`), Fritts workspace (`.fritts`).
- **Full-spec Heidelberg `.fh` parser** — Optional `FH2.1`/`FH1.0` magic, `;` comments, case-insensitive `KEY`/`DATEBEGIN`/`UNIT`/`DENDRO` keyword headers, decadal blocks with year labels, single-column format, multi-series files (separated by `KEY=`), `-9999` missing-value markers, `9999` stop code, `UNIT=`-based precision divisor. Compatible with `dplR::read.fh`.
- **Interactive plotting** — PyQtGraph canvas with smooth zoom, pan, multi-series overlay, and skeleton plot mode.
- **Statistical cross-dating** — Baillie-Pilcher t-value, Hollstein t-value, Gleichläufigkeit (GLK) with Buras-Wilmking (2015) correction, and sliding-window analysis.
- **COFECHA-style Quality Control** — Leave-one-out master chronology, Yule-Walker AR(p) prewhitening (AIC order selection, biased autocovariance matching `dplR::ar.yw`), segment correlation with Spearman's rho and p-value thresholding. Runs on a `QThread` so the UI stays responsive. Adjustable alpha (0.001 step).
- **Detrending** — Mean, negative exponential, Hugershoff, cubic spline (with optional `period` Butterworth low-pass), and Regional Curve Standardisation (RCS) using Tukey biweight, with optional `pith_offsets` and `min_depth`.
- **Chronology builder** — Interactive master chronology with real-time EPS and R-bar metrics. Biweight robust mean averaging. Build runs on a background thread.
- **Standard EPS** — Wigley/Briffa (1984) formula `EPS = (n_trees · r̄_bt) / (1 + (n_trees − 1) · r̄_bt)` with per-tree metadata; falls back with a warning when no `tree_id` is provided. R-bar is overlap-weighted (the `dplR::rwi.stats` default).
- **Hypothesis fuzz-tested Tucson parser** — 5 property-based tests (never-crash, round-trip, random bytes, unicode, embedded nulls) via `hypothesis`.
- **Classical ring detection** — Projection-profile boundary detection on scanned wood-section images (grayscale + RGB).
- **Geometric pith estimator** — Adjustable concentric-circle overlay for estimating missing distance to pith.
- **Session workspace** — Save and load complete project state to `.fritts` JSON files (all series, references, metadata). Loading a workspace properly rebinds all panels (`set_session`).
- **ITRDB data search** — Browse and download from the NOAA International Tree-Ring Data Bank via an SSRF-hardened client (HTTPS-only, hostname-based whitelist, redirect-blocking opener, streaming 100 MB cap, limit clamped to 1–45).
- **R/dplR export** — Generate companion `.R` scripts for advanced analysis in R's `dplR` package (with configurable `detrend_method` and `biweight` parameters).
- **Preferences** — DPI and theme settings persisted via `QSettings`; actually read on startup.
- **Full undo/redo via CommandStack** — Every UI mutation goes through commands. `AddSeriesCommand` wraps all import call sites (file import, image extraction, ITRDB download). 200-step undo history.
- **QThread workers** — `CrossDateWorker`, `DetrendWorker`, `BuildChronologyWorker`, and `QCWorker` move long-running analyses off the GUI thread with modal progress dialogs. `MainWindow.closeEvent` waits on any running workers.
- **Sample data** — Two synthetic 100-year series in `sample_data/example.rwl` for evaluation.

## Quick Start

```bash
# Install from PyPI
pip install fritts

# Launch
fritts
```

Or from source:

```bash
git clone https://github.com/mabo-du/fritts.git
cd fritts
pip install -e ".[dev]"
fritts
```

### First-run workflow

1. **Import data**: `File > Import Series…` (`Ctrl+Shift+I`) — select a `.rwl`, `.fh`, or `.xml` file.
2. **Explore**: Click series in the Series List to view statistics. Toggle visibility with checkboxes.
3. **Cross-date**: Right-click a floating series → "Cross-Date Series", or `Analysis > Cross-Date…` (`Ctrl+D`).
4. **Detrend**: `Analysis > Detrend Series…` — remove biological growth trends before chronology building.
5. **Build chronology**: `Analysis > Build Master Chronology (Auto)` (`Ctrl+B`) — create a master curve from reference series.
6. **QC**: `Analysis > Quality Control (COFECHA)…` — run COFECHA-style quality control with adjustable alpha threshold.
7. **Save**: `File > Save Workspace` (`Ctrl+S`) — save your session as a `.fritts` project file.
8. **Export**: `File > Export…` (`Ctrl+E`) — export as Tucson `.rwl`, TRiDaS `.xml`, or `File > Generate R Script (dplR)…` for an R/dplR companion script.

## Installation

### From PyPI (recommended)

```bash
pip install fritts
fritts
```

### From source (development)

```bash
git clone https://github.com/mabo-du/fritts.git
cd fritts

# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate  # Linux/macOS
# .venv\Scripts\activate   # Windows

# Install with dev dependencies
pip install -e ".[dev]"

# Launch
fritts
```

### Standalone binaries

Pre-built executables are available from the [GitHub Releases](https://github.com/mabo-du/fritts/releases) page:

| Platform | File | Notes |
|----------|------|-------|
| **Linux** | `fritts-linux-x64` + `Fritts-x86_64.AppImage` | AppImage is portable |
| **macOS** | `fritts-macos-arm64` | Apple Silicon (M1+) |
| **Windows** | `fritts-windows-x64.exe` | Standalone executable |

### System dependencies

**Ubuntu/Debian**:

```bash
sudo apt install libxcb-cursor0 libegl1 libgl1
```

**Fedora**:

```bash
sudo dnf install qt6-qtbase-gui
```

`libegl1` is required for PyQt6 GUI rendering on Linux. CI installs `libegl1 libgl1 libxkbcommon-x11-0 xvfb` and runs the GUI test suite under `xvfb-run -a` (see `.github/workflows/ci.yml`).

## Supported Formats

| Format | Extensions | Read | Write | Description |
|--------|-----------|------|-------|-------------|
| Tucson Decadal | `.rwl`, `.tuc`, `.crn` | ✅ | ✅ | Most common tree-ring format. 0.01mm or 0.001mm precision, auto-detected from stop code (`999` or `-9999`). Two-pass reader correctly distinguishes ITRDB `-9999` missing values from stop codes. |
| Heidelberg | `.fh` | ✅ | — | TSAP-Win / ITRDB Heidelberg archive format. **Full-spec reader (compatible with `dplR::read.fh`)**: optional `FH2.1`/`FH1.0` magic, `;` comments, case-insensitive `KEY`/`DATEBEGIN`/`UNIT`/`DENDRO` keyword headers, decadal blocks with 4-digit year labels, single-column format, multi-series files (separated by `KEY=`), `-9999` missing-value markers, `9999` positive stop code, `0` treated as a real measurement (NOT missing). Reads `UNIT` header for divisor; defaults to `/100` with a warning. The legacy simplified-subset `HEADER:`/`DATA:` format is still parsed via a backward-compat fallback. |
| TRiDaS XML | `.xml` | ✅ | ✅ | International standard (TRiDaS 1.2.2). Handles astronomical BCE convention. `<values>` filtered by `<variable normalTridas="Ring Width">`. NaN writes `<value/>` (not `0`). Optional `metadata` dict for project/object/sample/radius titles. |
| Fritts Workspace | `.fritts` | ✅ | ✅ | JSON project file preserving all series, references, metadata, and format version. `np.isfinite` filtering catches NaN, +inf, and -inf for valid JSON. |

### Precision handling

- **Tucson**: Stop code `999` → 0.01 mm precision (divide by 100). Stop code `-9999` (in the series' last decade line) → 0.001 mm precision (divide by 1000). A `-9999` *mid-series* is the ITRDB missing-value marker (treated as NaN, not a stop code). Mixed stop codes warn and use the dominant value as fallback.
- **TRiDaS**: Unit attribute determines precision (`1/100th millimetres` → /100, `micrometre` → /1000, `millimetre` → /1). Only `<value>` blocks whose `<variable normalTridas="Ring Width">` is collected (others are skipped).
- **Heidelberg**: Reads `UNIT` header (`1/100 mm` / `1/100mm` / `0.01 mm` → /100, `1/1000 mm` / `1/1000mm` / `micron` / `micrometre` / `micrometer` → /1000, `mm` → /1). Defaults to /100 with a warning if no unit is found. Missing values are `-9999` or empty tokens (→ NaN); `0` is a real measurement (NOT missing). The positive `9999` is the stop code (terminates a series); a mid-series `-9999` is a missing-value marker (NaN), not a stop code — this differs from the Tucson convention. The legacy simplified-subset `HEADER:`/`DATA:` format is still parsed via a backward-compat fallback.

## Dependencies

| Package | Minimum | Purpose |
|---------|---------|---------|
| PyQt6 | 6.6 | GUI framework |
| pyqtgraph | 0.13 (pin `<0.14`) | GPU-accelerated interactive plotting |
| pandas | 2.1 | Data manipulation and alignment |
| numpy | 1.26 | Numerical computation |
| scipy | 1.12 | Statistical algorithms (Pearson, Spearman, splines, Butterworth) |
| lxml | 5.0 | TRiDaS XML parsing |
| tifffile | 2024.1 | Memory-mapped TIFF loading for large images |

### Development dependencies

| Package | Minimum | Purpose |
|---------|---------|---------|
| pytest | 8.0 | Test runner |
| pytest-qt | 4.3 | Qt widget interaction tests |
| ruff | 0.4 | Linting and formatting |
| hypothesis | 6.0 | Property-based fuzz tests (Tucson parser) |

All dependencies have explicit upper bounds in `pyproject.toml` to prevent breaking on future major releases. `pyqtgraph` is pinned to `>=0.13,<0.14` (version 0.14 removed `PlotItem.autoRangeEnabled`, which the app relies on).

## Usage

### Launching

```bash
fritts
```

Or directly:

```bash
python -m dendro.main
```

### The Interface

The main window is divided into three panels:

| Panel | Location | Content |
|-------|----------|---------|
| **Series List** | Left | All loaded series with checkboxes, year ranges, visibility toggles. Right-click for context menu (Cross-Date, Set as Reference, Remove). |
| **Plot Area** | Centre | PyQtGraph canvas showing ring-width curves. Multi-series overlay with zoom/pan. |
| **Stats Panel** | Right | Selected series statistics: mean, standard deviation, min/max, year range, ring count. After cross-dating: t-values, GLK, overlap. |

### Series List operations

| Action | How |
|--------|-----|
| Select a series | Left-click → shows stats in right panel |
| Toggle visibility | Checkbox |
| Cross-date | Right-click → "Cross-Date Series" |
| Set as reference | Right-click → "Set as Reference" |
| Remove | Right-click → "Remove Series" (undoable via `RemoveSeriesCommand`) |
| Shift in time | Arrow keys (← →) with active selection. Clamped to ±10,000 years. |
| Snap to offset | Click a proposed start year in the cross-date results panel |

### Plot controls

| Control | Action |
|---------|--------|
| Left-click + drag | Pan |
| Scroll wheel | Zoom in/out |
| Ctrl + scroll | Zoom X-axis only |
| Shift + scroll | Zoom Y-axis only |
| Right-click | Context menu |
| F key | Zoom to fit |

## Cross-Dating Methods

Fritts provides three complementary statistics for cross-dating:

### Baillie-Pilcher t-value (`t_bp`)

A 5-year running mean is applied to both series before computing Pearson's r and converting to a t-statistic: `t = r · √((n−6)/(1−r²))`. This pre-smoothing reduces the influence of high-frequency noise and emphasises decadal-scale patterns.

- **Degrees of freedom**: n−6 (4 lost to the running mean, 2 to bivariate correlation).
- **Reference**: Baillie & Pilcher (1973), *Tree-Ring Bulletin* 33:7–14.
- **Typical significance**: t > 3.5 for 50-year overlap.

### Hollstein t-value (`t_ho`)

Each series is transformed to Wuchswerte (growth-change values) before correlation: `Wᵢ = 100 · ln(xᵢ / xᵢ₋₁)`. This year-over-year ratio transform emphasises short-term growth changes. The canonical formula matches `dendroNetwork::wuchswerte()` (Hollstein 1980, pp 14–15).

- **Degrees of freedom**: n−3 (1 lost to the lag, 2 to bivariate correlation).
- **Reference**: Hollstein (1980), *Mitteleuropäische Eichenchronologie*.

### Gleichläufigkeit (GLK)

GLK measures the percentage of years where both series show the same direction of change (up/up or down/down). Implemented with the **Buras-Wilmking (2015) correction**:

- Year-pairs where **both** series show zero change count as a synchronous match.
- Year-pairs where **only one** series shows zero change are excluded.
- Z-score and p-value are computed using the exact binomial test (n < 30) or normal approximation (n ≥ 30).
- **Reference**: Buras & Wilmking (2015), *Dendrochronologia* 33:42–48.

### Interpreting results

| Statistic | Threshold | Meaning |
|-----------|-----------|---------|
| t_bp / t_ho | > 3.5 | Strong match, series likely correctly dated |
| t_bp / t_ho | 2.5–3.5 | Possible match, investigate further |
| t_bp / t_ho | < 2.5 | Weak match |
| GLK | > 60% | Good trend agreement |
| GLK | > 70% | Very strong trend agreement |
| p-value | < 0.05 | Statistically significant |

### Sliding window cross-dating

`Analysis > Cross-Date…` (`Ctrl+D`) runs `crossdate_sliding()` which slides the sample across the reference at every position where the overlap exceeds the minimum threshold (UI default 50 years; module-level floor 30). At each position, all three statistics (t_bp, t_ho, GLK) are computed on the overlapping segment. Results are displayed as a table ranked by t_bp. The computation runs on a `CrossDateWorker` (`QThread`) with a modal progress dialog.

## COFECHA-Style Quality Control

`Analysis > Quality Control (COFECHA)…` runs a COFECHA-style analysis following `dplR::corr.series.seg()` conventions:

1. **Leave-one-out master**: For each target series, a master chronology is built from the mean (or biweight robust mean) of all other series.
2. **AR prewhitening**: Each full-length series is prewhitened via Yule-Walker AR(p) with AIC order selection (max order 5, AIC uses `n − order`), removing autocorrelation that would inflate apparent significance. The autocovariance uses the biased `1/n` estimator (matching `dplR::ar.yw`) and the residual lag-reversal bug is fixed.
3. **Sliding windows**: Fixed-length segments (default 50 years, 25-year overlap) are correlated against the master.
4. **Spearman's rho with p-value**: Segments are flagged when `p > α` (default α = 0.05). This adapts to varying segment lengths and df loss from prewhitening — unlike COFECHA's hardcoded r < 0.32 threshold which only applies to n=50.
5. **Background thread**: QC runs on a `QCWorker` (`QThread`) so the UI stays responsive. An indeterminate progress bar is shown while the worker runs.
6. **Adjustable alpha**: The QC dialog provides an alpha spinbox (0.001–1.0, step 0.001) with a "Run QC" button to re-run at different thresholds.

## Chronology Building

`Analysis > Build Master Chronology (Auto)` (`Ctrl+B`) creates a master chronology from selected reference series. The build runs on a `BuildChronologyWorker` (`QThread`) with a modal progress dialog:

1. Select reference series.
2. Choose averaging method: **Mean** or **Biweight robust mean** (resistant to outliers, recommended).
3. The chronology is built with EPS and R-bar calculated in real time and stored in the chronology's `metadata` dict:
   - **R-bar**: Overlap-weighted mean of pairwise correlations (Pearson r) — the `dplR::rwi.stats` default. When `tree_ids` are provided, only *between-tree* pairs are included.
   - **EPS** (Expressed Population Signal): the standard **Wigley, Briffa & Jones (1984)** formula `EPS = (n_trees · r̄_bt) / (1 + (n_trees − 1) · r̄_bt)`, where `n_trees` is the number of *distinct trees* and `r̄_bt` is the overlap-weighted mean of *between-tree* pairwise correlations (within-tree correlations are excluded — cores from the same tree share biological signal and would inflate EPS). Threshold for a well-represented chronology: ≥ 0.85.
4. The chronology appears in the Series List as `CHRONOLOGY`.
5. Right-click → "Cross-Date Series" to date floating series against the master chronology.

### EPS requires per-tree metadata

EPS requires per-tree metadata (set `tree_id` on each series' `metadata` dict — e.g. `RingWidthSeries("A1", 2000, widths, metadata={"tree_id": "A"})`) for the standard Wigley/Briffa (1984) formula. When `tree_id` is present on at least one series, `build_chronology` and `calculate_eps` automatically group cores by tree, exclude within-tree pairs from `r̄_bt`, and use the distinct-tree count as `n_trees`. Without `tree_id` metadata, a fallback all-pairs formula is used (with a `WARNING` log message): every series is treated as its own tree, `n = len(series_list)`, and `r̄ = calculate_rbar(series_list)` (the overlap-weighted mean of *all* pairwise correlations). This fallback over-estimates EPS when multiple cores per tree exist; provide `tree_id` metadata for the standard formula.

## Image Measurement

`File > Import Image Scan…` opens the wood-section image viewer:

1. **Load an image** — JPEG, PNG, or TIFF. Files >500 MB use memory-mapped tifffile I/O for efficient loading.
2. **Set DPI** — Calibrate the image resolution for accurate millimeter measurements. The default DPI is read from `QSettings` (`image/defaultDpi`, default 1200).
3. **Detect rings** — Click "Auto-Detect Rings" to run the classical projection-profile boundary detection algorithm (handles both grayscale and RGB input; edge-replication padding avoids spurious edge peaks).
4. **Manual markers** — Left-click to place/adjust ring boundary markers. Bounds-check uses `boundingRect()` so markers always land inside the image.
5. **Extract series** — Click "Extract Series" with ≥2 markers to export ring-width measurements. Series IDs are validated (≤8 alphanumeric characters); the default series ID is `IMG_01`.
6. **Pith estimator** — Toggle concentric-circle overlay to estimate missing distance to pith.

The channel order for loaded images is **RGB** (matching pyqtgraph's `makeARGB` expectations). Grayscale images are automatically broadcast to 3-channel.

## Session Workspace

`File > Save Workspace` (`Ctrl+S`) serialises the entire session (all series, detrended indices, reference flags, metadata) to a `.fritts` JSON file:

- NaN, +inf, and -inf values → JSON `null` for standard compliance (via `np.isfinite`).
- `format_version` field for forward/backward compatibility.
- `File > Open Workspace` (`Ctrl+O`) to reload. Loading a workspace now properly rebinds all panels (`SeriesListPanel`, `SeriesView`, `ChronologyBuilderDock`) to the new session via `set_session` — disconnecting old-session signals, reconnecting new-session signals, and rebuilding the view from the new data.
- `File > Save Workspace As…` (`Ctrl+Shift+S`) for versioned saves. The path is committed only after the write succeeds.

## ITRDB Integration

`File > Import from ITRDB…` connects to the [NOAA International Tree-Ring Data Bank](https://www.ncei.noaa.gov/products/paleoclimatology/tree-ring) API. The ITRDB client is SSRF-hardened:

- **HTTPS-only** — the `http://` scheme is rejected.
- **Hostname-based whitelist** — `ncei.noaa.gov` and `*.ncei.noaa.gov` (proper domain-boundary check; rejects suffix-spoofing like `fake-ncei.noaa.gov`). `parsed.hostname` strips port and userinfo.
- **Redirect-blocking opener** — a `urllib.request` opener with a no-redirect handler prevents 3xx-based SSRF bypasses.
- **Streaming 100 MB size cap** — downloads are streamed in 64 KB chunks; the download is aborted with `RuntimeError` if the total exceeds 100 MB (works even when `Content-Length` is absent).
- **Limit clamping** — the `limit` parameter is clamped to `[1, 45]` (the NOAA API rejects `limit ≥ 46` with HTTP 500).
- **User-Agent**: `Fritts/{version}` for API identification.
- **SSL context** — `ssl.create_default_context()` for certificate verification.

The ITRDB dialog emits a `series_ready_for_import(list)` signal on download; MainWindow receives it and submits an `AddSeriesCommand` so the import is undoable. Dialog threads are cleaned up on close.

API endpoint: `https://www.ncei.noaa.gov/access/paleo-search/study/search.json`

## Keyboard Shortcuts

| Shortcut | Action |
|----------|--------|
| `Ctrl+O` | Open Workspace |
| `Ctrl+S` | Save Workspace |
| `Ctrl+Shift+S` | Save Workspace As |
| `Ctrl+Shift+I` | Import Series |
| `Ctrl+E` | Export |
| `Ctrl+,` | Preferences |
| `Ctrl+Z` | Undo |
| `Ctrl+Shift+Z` / `Ctrl+Y` | Redo |
| `Ctrl+D` | Cross-Date |
| `Ctrl+B` | Build Chronology |
| `F` | Zoom to Fit |
| `←` / `→` | Shift active series ±1 year (available after a Cross-Date has been run) |
| `Ctrl+Q` | Quit |

## Sample Data

The repository includes `sample_data/example.rwl` — two synthetic 100-year series (SAMPLEA, SAMPLEB) generated from an age-related exponential trend with AR(1) noise. The decadal stop-code lines were fixed to align with the Tucson 8-char series-ID + 4-char year column format (the old `SAMPLEA2050 999` lines were silently misread as `series_id="SAMPLEA2"`, `year=50`). These are suitable for:

- Testing import/export workflows
- Evaluating cross-dating statistics
- Practicing chronology building
- Familiarisation with the interface

```bash
fritts
# File > Import Series… → select sample_data/example.rwl
```

## Project Structure

```
fritts/
├── assets/                  # Static assets (screenshots, illustrations)
│   ├── fritts.png           # Screenshot
│   └── fritts_hero.png      # Dendrochronology-themed illustration
├── docs/
│   ├── USER_GUIDE.md        # Comprehensive user documentation
│   ├── scope.md             # Project scope and roadmap
│   └── research-papers/     # Reference papers
├── sample_data/             # Synthetic ring-width files
│   └── example.rwl          # 2 series, 100 years each
├── src/
│   └── dendro/
│       ├── io/              # Format parsers
│       │   ├── tucson.py    # Tucson decadal (.rwl) read/write
│       │   ├── heidelberg.py# Full TSAP-Win .fh reader
│       │   ├── tridas.py    # TRiDaS XML read/write
│       │   ├── itrdb.py     # NOAA ITRDB API client (SSRF-hardened)
│       │   └── export_r.py  # dplR-compatible R script generator
│       ├── models/
│       │   ├── series.py    # RingWidthSeries (frozen dataclass)
│       │   ├── session.py   # SessionManager with serialization
│       │   └── commands.py  # Command pattern (undo/redo, AddSeriesCommand)
│       ├── stats/
│       │   ├── crossdate.py  # t_bp, t_ho, GLK, sliding cross-date
│       │   ├── chronology.py # Chronology builder, rbar, EPS (Wigley/Briffa 1984)
│       │   ├── detrend.py    # Mean, neg exp, spline, RCS (biweight + pith offsets)
│       │   ├── quality_control.py  # COFECHA-style QC (AR prewhitening)
│       │   └── ai_segmentation.py  # Classical ring detection
│       ├── ui/
│       │   ├── main_window.py      # Application window + QThread workers
│       │   ├── series_list.py      # Series list panel
│       │   ├── series_view.py      # Plot panel
│       │   ├── image_view.py       # Image measurement
│       │   ├── chronology_builder.py # Chronology dock
│       │   ├── preferences_dialog.py # Settings dialog
│       │   ├── qc_dialog.py        # QC report dialog (QCWorker)
│       │   ├── itrdb_dialog.py     # ITRDB search/download dialog
│       │   ├── detrend_dialog.py   # Detrend method dialog
│       │   ├── dialogs.py          # CrossDate/Import/Export dialogs
│       │   └── stats_panel.py      # Stats display
│       └── main.py           # Application entry point
├── tests/                    # Test suite (230+ tests)
│   ├── test_regression.py             # Statistics + models regression tests
│   ├── test_session_serialization.py  # .fritts save/load round-trip
│   ├── test_commands.py               # Command pattern (incl. AddSeriesCommand)
│   ├── test_crossdate.py              # Cross-dating edge cases
│   ├── test_tucson_parser.py          # Tucson read/write round-trip
│   ├── test_io_formats.py             # Heidelberg (.fh full-spec + legacy) + TRiDaS
│   ├── test_tucson_fuzz.py            # Hypothesis fuzz tests (Tucson parser)
│   ├── test_itrdb.py                  # ITRDB API client (mocked HTTP, SSRF)
│   ├── test_detrend.py                # Detrending methods
│   ├── test_export_r.py               # R/dplR script generator
│   ├── test_ui_fixes.py               # Source-inspection tests (no Qt required)
│   └── test_ui_smoke.py               # Qt widget interaction tests (pytest-qt)
├── .github/workflows/
│   ├── ci.yml                # Lint + test (xvfb-run for GUI tests)
│   ├── publish.yml           # PyPI publishing
│   └── release.yml           # GitHub Release binaries
├── pyproject.toml            # Build configuration
└── README.md
```

## Development

```bash
# Install with dev dependencies
pip install -e ".[dev]"

# Run tests (230+ tests)
pytest

# Run GUI tests under a virtual display (Linux CI / headless dev)
xvfb-run -a pytest

# Run linting
ruff check src/ tests/

# Format code
ruff format src/ tests/
```

### Test organization

| File | Coverage |
|------|----------|
| `test_regression.py` | FR-specific regression tests (statistics, models, signals, EPS, biweight, RCS, AR prewhitening) |
| `test_session_serialization.py` | `.fritts` save/load round-trip, version validation, inf→null serialization, no-direct-`add_series`-in-UI guard |
| `test_commands.py` | Command pattern (`DetrendCommand`, `AddSeriesCommand`, `BuildChronologyCommand`, undo/redo) |
| `test_crossdate.py` | Cross-dating edge cases (no overlap, min_overlap, identical series) |
| `test_tucson_parser.py` | Tucson read/write round-trip (BCE, non-decade starts, two-pass `-9999` handling) |
| `test_io_formats.py` | Heidelberg (`.fh` full-spec + legacy subset) and TRiDaS parser tests |
| `test_tucson_fuzz.py` | Hypothesis fuzz tests for the Tucson parser (never-crash, round-trip, random bytes, unicode, embedded nulls) |
| `test_detrend.py` | Detrending methods (mean, neg exp, spline, RCS with pith offsets) |
| `test_itrdb.py` | ITRDB API client (mocked HTTP, SSRF hardening, host whitelist, redirect block, size cap) |
| `test_export_r.py` | R/dplR script generator (path escaping, detrend method, biweight flag) |
| `test_ui_fixes.py` | Source-inspection tests for UI fixes (no Qt required — runs in any CI) |
| `test_ui_smoke.py` | Qt widget interaction tests (pytest-qt; 8 qtbot tests for menu/dialog/worker flows) |

The full suite collects 234 tests; 213 pass in the dev sandbox, 21 are skipped (require `DISPLAY` + `libegl1`), 1 pre-existing failure is the `libEGL.so.1` import issue (works in CI, which installs `libegl1` and runs under `xvfb-run`).

## User Guide

See [docs/USER_GUIDE.md](docs/USER_GUIDE.md) for comprehensive documentation covering:

- Installation troubleshooting
- Importing and exporting data
- Cross-dating workflows
- Detrending methods
- Chronology building with EPS/R-bar interpretation
- COFECHA-style Quality Control (prewhitening, alpha threshold)
- ITRDB data search
- Image measurement and ring detection
- Keyboard shortcuts reference
- Troubleshooting common issues

## Target Users

- **Dendrochronologists** dating archaeological timbers and historical structures.
- **Climate researchers** building proxy records from tree rings.
- **Wood specialists** in archaeology, heritage, and conservation.
- **Students** learning tree-ring analysis methods.

## Tech Stack

| Component | Technology |
|-----------|------------|
| GUI | PyQt6 (Qt 6.6+) |
| Visualization | PyQtGraph (0.13.x, GPU-accelerated) |
| Data | Pandas, NumPy |
| Statistics | SciPy (Pearson r, Spearman rho, splines, Butterworth low-pass) |
| XML | lxml (TRiDaS parsing, secure configuration) |
| Image I/O | tifffile (large TIFF memory-mapped loading) |
| Testing | pytest, pytest-qt, pytest-mock, hypothesis |
| Linting | ruff |
| Build | setuptools, PyInstaller, AppImage |

## Known Issues

### NOAA ITRDB API limit

The ITRDB search API returns HTTP 500 errors when the `limit` parameter is 46 or higher. Fritts clamps `limit` to `[1, 45]` (with a `WARNING` log message when clamped) and defaults to `limit=30`. If you need more results, run multiple searches with different keywords.

### pyqtgraph version

Fritts requires `pyqtgraph>=0.13,<0.14`. Version 0.14 removed `PlotItem.autoRangeEnabled`, which Fritts' plotting code relies on — interactions with affected plot items raise `AttributeError`. The pin is enforced in `pyproject.toml`, and a startup warning is shown if a ≥0.14 version is detected at runtime. The previous class-level monkey-patch of `pg.PlotItem.autoRangeEnabled` in `series_view.py` was removed (it is no longer needed with the pin in place).

### Large image loading

For TIFF files >500 MB, the QImage path has a 2× memory overhead (RGBA decode + numpy copy). Fritts automatically switches to tifffile memory-mapped I/O for files above this threshold, avoiding the double allocation. For extremely large files (>2 GB), ensure sufficient system memory for pyqtgraph's texture upload.

### Undo stack

The undo stack is limited to 200 commands (backed by `collections.deque(maxlen=200)`). When exceeded, the oldest commands are auto-evicted in O(1). This prevents unbounded memory growth during long sessions.

## Limitations

### Direct dplR verification of EPS / spline / RCS / biweight is pending

Direct numerical verification of EPS, spline detrending, RCS, and Tukey biweight against `dplR` is pending — R could not be installed in the development sandbox (no sudo available; the system R packages require root to install). The implementations match the published definitions:

- **EPS**: standard **Wigley, Briffa & Jones (1984, *International Journal of Climatology* 4:201–213)** formula `EPS = (n_trees · r̄_bt) / (1 + (n_trees − 1) · r̄_bt)`, with `r̄_bt` the overlap-weighted mean of *between-tree* pairwise correlations (within-tree pairs excluded) and `n_trees` the distinct-tree count. Verified against closed-form expectations on synthetic data (see `tests/test_regression.py::test_eps_*`).
- **Tukey biweight**: uses the *raw* MAD (no `1.4826` consistency scaling) with `c = 9.0`, matching `dplR::tbrm()`. Verified by property test.
- **AR(p) prewhitening**: biased autocovariance (`1/n`, matching `dplR::ar.yw`), AIC uses `n − order`, lag indices correctly reversed (`np.dot(phi[::-1], …)`).
- **RCS**: Tukey biweight robust mean per age class (matching `dplR::rcs`), with optional `pith_offsets` and `min_depth` parameters.

Numerical equivalence to `dplR`'s output has not been confirmed by direct comparison; the regression suite verifies each formula against closed-form expectations on synthetic data instead.

### Spline detrend `period` parameter is a Butterworth approximation

The `period` parameter on `detrend_series`/`fit_spline` uses a 2nd-order Butterworth low-pass filter (via `scipy.signal.butter` + `filtfilt`) at `1/period` cycles/year, with NaN-gap interpolation. This is an approximation to `dplR`'s FFT-based `ffcsaps` smoothing spline; the legacy `UnivariateSpline`-based stiffness behaviour is preserved when `period is None`.

### EPS without tree metadata uses a fallback all-pairs formula

When `tree_id` metadata is not provided on any series, `calculate_eps` falls back to the all-pairs formula `(n · r̄) / (1 + (n − 1) · r̄)` with `n = len(series_list)` and `r̄ = calculate_rbar(series_list)`, emitting a `WARNING` log message. This over-estimates EPS when multiple cores per tree exist; provide `tree_id` metadata for the standard formula.

### GUI fixes are runtime-verified in CI, not the dev sandbox

The QThread workers (`CrossDateWorker`, `DetrendWorker`, `BuildChronologyWorker`, `QCWorker`), `set_session` panel rebinding, and the 8 qtbot interaction tests are syntax-verified (via `py_compile` and `ast.parse`) and behaviourally verified by source-inspection tests in `tests/test_ui_fixes.py`. They are NOT runtime-verified in the dev sandbox because `libEGL.so.1` is missing (and not installable without sudo). The CI workflow (`.github/workflows/ci.yml`) installs `libegl1 libgl1 libxkbcommon-x11-0 xvfb` and runs the full suite under `xvfb-run -a`, so on GitHub Actions the UI tests execute against real Qt widgets.

## Contributing

1. Fork the repository on [GitHub](https://github.com/mabo-du/fritts) or [GitLab](https://gitlab.com/mabodu/fritts).
2. Create a feature branch from `master`.
3. Make your changes with tests. Every UI mutation should go through a `Command` subclass and the `CommandStack` — see `src/dendro/models/commands.py` for the pattern. Direct `self._session.add_series(...)` / `remove_series(...)` / `replace_series(...)` calls in `src/dendro/ui/` are flagged by `tests/test_session_serialization.py::test_no_direct_session_mutation_in_ui`.
4. Run the full test suite: `pytest` (or `xvfb-run -a pytest` on headless Linux to also exercise the Qt widget tests).
5. Run `ruff check src/ tests/` and `ruff format src/ tests/`.
6. Submit a merge request / pull request.

The full history of fixes applied during the v0.3.0 review pass is recorded in `worklog.md` at the repository root (79 code-review findings, 100% fix rate). See also `docs/research-papers/Dendrochronology Algorithm Reference Validation.md` for the algorithm-by-algorithm comparison against `dplR` and COFECHA.

## License

MIT — see [LICENSE](LICENSE).

---

*Fritts — Named in honor of Harold C. Fritts (1930–2024), pioneer of dendroclimatology.*
