Open-source desktop tool for tree-ring cross-dating, measurement, and master chronology building.
Project description
Fritts — Dendrochronology Analysis Platform
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.
Repository: GitHub · GitLab
Package: fritts on PyPI
Table of Contents
- Why Fritts?
- Features
- Quick Start
- Installation
- Supported Formats
- Dependencies
- Usage
- Cross-Dating Methods
- COFECHA-Style Quality Control
- Chronology Building
- Image Measurement
- Session Workspace
- ITRDB Integration
- Keyboard Shortcuts
- Sample Data
- Project Structure
- Development
- User Guide
- Target Users
- Tech Stack
- Known Issues
- Limitations
- Contributing
- 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
.fhspec (compatible withdplR::read.fh), Tucson decadal read/write, TRiDaS read/write. - COFECHA-compatible QC — Sliding-window correlation with AR prewhitening and Spearman significance (matching
dplRdefaults). 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
AddSeriesCommandfor 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
.fhparser — OptionalFH2.1/FH1.0magic,;comments, case-insensitiveKEY/DATEBEGIN/UNIT/DENDROkeyword headers, decadal blocks with year labels, single-column format, multi-series files (separated byKEY=),-9999missing-value markers,9999stop code,UNIT=-based precision divisor. Compatible withdplR::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 aQThreadso the UI stays responsive. Adjustable alpha (0.001 step). - Detrending — Mean, negative exponential, Hugershoff, cubic spline (with optional
periodButterworth low-pass), and Regional Curve Standardisation (RCS) using Tukey biweight, with optionalpith_offsetsandmin_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 notree_idis provided. R-bar is overlap-weighted (thedplR::rwi.statsdefault). - 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
.frittsJSON 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
.Rscripts for advanced analysis in R'sdplRpackage (with configurabledetrend_methodandbiweightparameters). - Preferences — DPI and theme settings persisted via
QSettings; actually read on startup. - Full undo/redo via CommandStack — Every UI mutation goes through commands.
AddSeriesCommandwraps all import call sites (file import, image extraction, ITRDB download). 200-step undo history. - QThread workers —
CrossDateWorker,DetrendWorker,BuildChronologyWorker, andQCWorkermove long-running analyses off the GUI thread with modal progress dialogs.MainWindow.closeEventwaits on any running workers. - Sample data — Two synthetic 100-year series in
sample_data/example.rwlfor evaluation.
Quick Start
# Install from PyPI
pip install fritts
# Launch
fritts
Or from source:
git clone https://github.com/mabo-du/fritts.git
cd fritts
pip install -e ".[dev]"
fritts
First-run workflow
- Import data:
File > Import Series…(Ctrl+Shift+I) — select a.rwl,.fh, or.xmlfile. - Explore: Click series in the Series List to view statistics. Toggle visibility with checkboxes.
- Cross-date: Right-click a floating series → "Cross-Date Series", or
Analysis > Cross-Date…(Ctrl+D). - Detrend:
Analysis > Detrend Series…— remove biological growth trends before chronology building. - Build chronology:
Analysis > Build Master Chronology (Auto)(Ctrl+B) — create a master curve from reference series. - QC:
Analysis > Quality Control (COFECHA)…— run COFECHA-style quality control with adjustable alpha threshold. - Save:
File > Save Workspace(Ctrl+S) — save your session as a.frittsproject file. - Export:
File > Export…(Ctrl+E) — export as Tucson.rwl, TRiDaS.xml, orFile > Generate R Script (dplR)…for an R/dplR companion script.
Installation
From PyPI (recommended)
pip install fritts
fritts
From source (development)
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 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:
sudo apt install libxcb-cursor0 libegl1 libgl1
Fedora:
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-9999mid-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
UNITheader (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-9999or empty tokens (→ NaN);0is a real measurement (NOT missing). The positive9999is the stop code (terminates a series); a mid-series-9999is a missing-value marker (NaN), not a stop code — this differs from the Tucson convention. The legacy simplified-subsetHEADER:/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
fritts
Or directly:
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:
- Leave-one-out master: For each target series, a master chronology is built from the mean (or biweight robust mean) of all other series.
- 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 biased1/nestimator (matchingdplR::ar.yw) and the residual lag-reversal bug is fixed. - Sliding windows: Fixed-length segments (default 50 years, 25-year overlap) are correlated against the master.
- 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. - Background thread: QC runs on a
QCWorker(QThread) so the UI stays responsive. An indeterminate progress bar is shown while the worker runs. - 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:
- Select reference series.
- Choose averaging method: Mean or Biweight robust mean (resistant to outliers, recommended).
- The chronology is built with EPS and R-bar calculated in real time and stored in the chronology's
metadatadict:- R-bar: Overlap-weighted mean of pairwise correlations (Pearson r) — the
dplR::rwi.statsdefault. Whentree_idsare 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), wheren_treesis the number of distinct trees andr̄_btis 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.
- R-bar: Overlap-weighted mean of pairwise correlations (Pearson r) — the
- The chronology appears in the Series List as
CHRONOLOGY. - 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:
- Load an image — JPEG, PNG, or TIFF. Files >500 MB use memory-mapped tifffile I/O for efficient loading.
- Set DPI — Calibrate the image resolution for accurate millimeter measurements. The default DPI is read from
QSettings(image/defaultDpi, default 1200). - 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).
- Manual markers — Left-click to place/adjust ring boundary markers. Bounds-check uses
boundingRect()so markers always land inside the image. - 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. - 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
nullfor standard compliance (vianp.isfinite). format_versionfield 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 viaset_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 API. The ITRDB client is SSRF-hardened:
- HTTPS-only — the
http://scheme is rejected. - Hostname-based whitelist —
ncei.noaa.govand*.ncei.noaa.gov(proper domain-boundary check; rejects suffix-spoofing likefake-ncei.noaa.gov).parsed.hostnamestrips port and userinfo. - Redirect-blocking opener — a
urllib.requestopener 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
RuntimeErrorif the total exceeds 100 MB (works even whenContent-Lengthis absent). - Limit clamping — the
limitparameter is clamped to[1, 45](the NOAA API rejectslimit ≥ 46with 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
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
# 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 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), withr̄_btthe overlap-weighted mean of between-tree pairwise correlations (within-tree pairs excluded) andn_treesthe distinct-tree count. Verified against closed-form expectations on synthetic data (seetests/test_regression.py::test_eps_*). - Tukey biweight: uses the raw MAD (no
1.4826consistency scaling) withc = 9.0, matchingdplR::tbrm(). Verified by property test. - AR(p) prewhitening: biased autocovariance (
1/n, matchingdplR::ar.yw), AIC usesn − order, lag indices correctly reversed (np.dot(phi[::-1], …)). - RCS: Tukey biweight robust mean per age class (matching
dplR::rcs), with optionalpith_offsetsandmin_depthparameters.
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
- Fork the repository on GitHub or GitLab.
- Create a feature branch from
master. - Make your changes with tests. Every UI mutation should go through a
Commandsubclass and theCommandStack— seesrc/dendro/models/commands.pyfor the pattern. Directself._session.add_series(...)/remove_series(...)/replace_series(...)calls insrc/dendro/ui/are flagged bytests/test_session_serialization.py::test_no_direct_session_mutation_in_ui. - Run the full test suite:
pytest(orxvfb-run -a pyteston headless Linux to also exercise the Qt widget tests). - Run
ruff check src/ tests/andruff format src/ tests/. - 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.
Fritts — Named in honor of Harold C. Fritts (1930–2024), pioneer of dendroclimatology.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fritts-0.3.0.tar.gz.
File metadata
- Download URL: fritts-0.3.0.tar.gz
- Upload date:
- Size: 174.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5425a8edf3fc9b1cfcb2edb0b3440756175a7235305e66a9a235a25111410d74
|
|
| MD5 |
91816c4aad160a224487842f0c03a6fd
|
|
| BLAKE2b-256 |
51b7f7c81eafe82c8b5399cd631c29a26fc9b265b8e67815c156d7604703341b
|
File details
Details for the file fritts-0.3.0-py3-none-any.whl.
File metadata
- Download URL: fritts-0.3.0-py3-none-any.whl
- Upload date:
- Size: 117.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ef9cb4643e525e9f3bdb3564358f3226eb542fea980ed1f8517603594020905
|
|
| MD5 |
06f60095c604d5ebdb588fb4c2b4bf59
|
|
| BLAKE2b-256 |
d6d757b7fad726f570846dc95533e079fe4b6bd1e8ce38b6cd0ea168b1409a3a
|