Metadata-Version: 2.5
Name: copra2
Version: 2.0.0rc3
Summary: COPRA2 — Constructing Proxy Records from Age Models (Python/Qt6 reimplementation)
Project-URL: Homepage, https://tocsy.pik-potsdam.de/copra2.php
Project-URL: Documentation, https://tocsy.pik-potsdam.de/copra2.php
Project-URL: Repository, https://gitlab.com/tocsy/copra
Project-URL: Bug Tracker, https://gitlab.com/tocsy/copra/-/issues
Author: Sebastian F. M. Breitenbach
Author-email: Norbert Marwan <norbert.marwan@pik-potsdam.de>
License: GPL-3.0-or-later
Keywords: Monte Carlo,age-depth model,geochronology,palaeoclimate,proxy record,speleothem
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: X11 Applications :: Qt
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.10
Requires-Dist: matplotlib>=3.7
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: scipy>=1.10
Provides-Extra: dev
Requires-Dist: pyside6>=6.5; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: gui
Requires-Dist: pyside6>=6.5; extra == 'gui'
Description-Content-Type: text/markdown

# COPRA2 (Python/Qt6)

Python reimplementation of COPRA — Constructing Proxy Records from Age Models.
The original MATLAB/Octave toolbox is in the top-level `copra/` folder; this
package (`copra_py/`) is the port. See `../ROADMAP.md` for the phased plan and
`../CLAUDE.m` for project principles.

Full user documentation (installation, GUI and CLI usage, data formats, output
files) is in [`docs/index.html`](docs/index.html) — open it in a browser.

## Install

The distribution name on PyPI is **`copra2`** (the name `copra` was already
taken); the import package and the `copra-cli` / `copra-gui` commands keep the
`copra` name.

```sh
pip install copra2          # command-line tool + Python API (no GUI)
pip install "copra2[gui]"   # add the PySide6/Qt6 desktop application
```

PySide6 is an **optional** dependency: the command-line tool (`copra-cli`) and the
`copra.core` API work without it, so old systems where PySide6 cannot be installed
can still use COPRA. `pip install "copra2[gui]"` additionally installs PySide6 and
enables the desktop app (`copra-gui`). For end users without Python, standalone
double-click apps are built per OS with PyInstaller (see *Packaging* below).

## Status

All ROADMAP phases (0–12) are implemented and tested: I/O, reversal
detection/treatment, hiatus handling, the Monte Carlo age model (including layer
counting), resampling, logging/results output, plotting, a command-line tool,
and the PySide6/Qt6 desktop application. All four MATLAB reference cases
(reversal, hiatus, layer-count on/off) are validated. Deliberate differences
from the original are recorded in `DIFFERENCES.md`.

## Desktop application (GUI)

A PySide6/Qt6 desktop app wraps the same pipeline:

```sh
copra-gui            # or:  python -m copra.gui
```

Pick the dating / proxy (/ layer-count) files, set parameters and any reversal
treatment, click **Check** to review reversals and hiatus candidates, **Run** to
compute the age model on a background thread (with a progress bar and Cancel),
and **Export** to write the results/ensemble/session/log. The Monte Carlo core is
unchanged — the GUI only calls into `copra.core` and `copra.plotting`.

A computed age model can be reused for other proxy records measured on the same
core: after a run, **File → Replace proxy record** applies the existing Monte
Carlo age model to a new proxy (no re-run), updating the proxy plots immediately.
COPRA then requires a new sample name on export so the original results are not
overwritten. **File → Open/Save session** load and store the full run (inputs,
parameters, treatment and seed) as JSON; **File → Close** starts a fresh project.

## Command-line usage

After `pip install -e .`, the `copra-cli` command runs the whole pipeline.

For a guided, step-by-step session like the original MATLAB `copra` (prompts for
files, parameters, reversal treatment, hiatus, then runs and saves):

```sh
copra-cli interactive     # or just: copra-cli
```

For scripted / batch use, drive it entirely with options (no prompts):

```sh
# Inspect a dataset: reversals + hiatus candidates, no simulation
copra-cli check --dating DATING.txt --proxy PROXY.txt

# Run the age model and write results/session/log (+ optional plots)
copra-cli run --dating DATING.txt --proxy PROXY.txt \
    --M 2000 --interp pchip --seed 42 --output-dir output --plots

# Reversal treatment (1-based point ids); repeat an id to widen its error again
copra-cli run --dating DATING.txt --proxy PROXY.txt \
    --remove-points 3 --increase-error 6 6 6

# Layer counting and an explicit hiatus
copra-cli run --dating DATING.txt --proxy PROXY.txt \
    --layercount LAYERS.txt --hiatus 389.5

# Reproduce a previous run exactly from its saved session
copra-cli reproduce output/d_<sample>_<date>.json
```

Options can also be supplied by a TOML file via `--config run.toml` (a `[copra]`
table with the same keys); explicit CLI options override the config. Each run
writes four files: a results `.txt` (mean/median + confidence quantiles per
age), an **ensemble `.csv`** with every Monte Carlo realisation (depth, proxy,
and all M age columns — the MATLAB `d.T_unc`), a JSON session (inputs,
parameters and the random seed), and a legacy `.m` log. `reproduce` reloads the
session and re-runs to a bit-identical result.

`check --plots` and the interactive mode display review figures of the dating
table (reversals coloured by severity) and the detected hiatus depths before you
choose which points to treat or where to split.

Try it on the bundled test data:

```sh
copra-cli run \
    --dating tests/data/layercount_none/inputs/testagemodel_good.txt \
    --proxy  tests/data/layercount_none/inputs/isotope-test-good.txt \
    --M 500 --seed 1 --samplename demo --output-dir /tmp/copra_demo --plots
```

## Data formats

Input files are plain whitespace- or comma-separated text (`.txt`/`.csv`) with
no column headers (comment lines starting with `#` or `%` are ignored). Depths
must be strictly increasing and unique (no duplicate depths, no NaNs).

- **Dating table** — three columns: `depth`, `age`, `age error` (1σ). One row
  per dated level. An optional fourth column makes the error *asymmetric* (column
  3 = lower, column 4 = upper 1σ error), sampled with a two-piece normal.
- **Proxy record** — two columns: `depth`, `proxy value`. An optional third
  column gives the 1σ proxy-value error (off by default). A constant sample size
  (GUI field / `--sample-size`) adds a uniform depth (distance) error; both are
  folded into the resampled proxy uncertainty.
- **Layer count** (optional) — three columns: `depth`, `age`, `depth error`.
  The layer-count age is *relative* to the first counted layer (first layer has
  age 0).

Each run writes four output files, whose names carry the sample name and the run
date in ISO `YYYY-MM-DD` form:

- `<sample>_<interp>_<date>.txt` — results table: age, proxy mean, age and proxy
  confidence quantiles, and depth (tab-separated, one header line).
- `<sample>_ensemble_<date>.csv` — the full Monte Carlo ensemble (`depth`,
  `proxy`, and one column per age realisation; the MATLAB `d.T_unc`).
- `d_<sample>_<date>.json` — the canonical session: inputs, parameters and the
  random seed. Feeding it back to `copra-cli reproduce` re-runs to a
  bit-identical ensemble.
- `log_COPRA_<date>.m` — a legacy MATLAB-readable run log.

## Packaging

Build a wheel/sdist with the standard PEP 517 tooling (Hatchling backend):

```sh
pip install build
python -m build          # writes dist/copra-<version>-py3-none-any.whl
```

The package version is defined once in `copra/__init__.py` and read dynamically
by the build; the application icon (`copra/gui/resources/copra_icon.png`) is
bundled into the wheel.

### Standalone desktop app

A double-clickable application is produced with PyInstaller from the bundled spec
(`packaging/copra.spec`), which embeds Python, PySide6 and the icon:

```sh
pip install pyinstaller
pyinstaller packaging/copra.spec        # run from copra_py/
```

The result appears under `dist/`:

- **macOS** — `dist/COPRA.app` (double-click to launch; icon `packaging/copra.icns`).
- **Windows** — `dist/COPRA/COPRA.exe`.
- **Linux** — `dist/COPRA/COPRA`.

Build the app on the target platform (PyInstaller does not cross-compile).

#### macOS: opening an unsigned, downloaded app

The `.app` is unsigned and not notarised. When it is downloaded from the web (or
otherwise copied with the quarantine flag), macOS Gatekeeper blocks it on first
launch with a message that the developer cannot be verified. To allow it:

1. Double-click `COPRA.app` once (the launch is blocked — this is expected).
2. Open **System Settings → Privacy & Security**, scroll to the **Security**
   section, and next to the COPRA message click **Open Anyway**.
3. Confirm with **Open** in the following dialog. macOS remembers the choice.

On older macOS versions, right-click the app → **Open** → **Open** also works.
Alternatively, clear the quarantine flag from a terminal:

```sh
xattr -dr com.apple.quarantine /path/to/COPRA.app
```

### Releases (GitLab, Free tier — no Premium needed)

On a version tag the CI (`../.gitlab-ci.yml`) builds the Linux (and, with a
runner, Windows) app and uploads it to the project's **Generic Package Registry**
(package `copra2` / version `<tag>`) using the built-in `CI_JOB_TOKEN` — no manual
token. The `release` job then creates a **GitLab Release** (Deploy → Releases)
whose assets link the three registry files, and `deploy:pypi` publishes the wheel
to PyPI. Package Registry and Releases are both on the Free tier.

macOS has no runner here, so build it locally and upload it to the *same* registry
with a **free Personal Access Token** (scope `api` or `write_package_registry`;
only Project/Group Access Tokens are Premium). The token is **never** put in the
script or in git — store it in a file outside the repo and the script reads it:

```sh
# one-time, outside the repository:
mkdir -p ~/.config/copra
printf '%s' 'glpat-xxxxxxxx' > ~/.config/copra/gitlab_token && chmod 600 ~/.config/copra/gitlab_token

# per release, from copra_py/:
pyinstaller packaging/copra.spec                       # dist/COPRA.app
PROJECT_ID=<id> packaging/package_macos.sh v2.0.0rc3   # zips + uploads
```

The token is read from `$TOKEN` (env), else `$TOKEN_FILE`, else
`~/.config/copra/gitlab_token`. So: push tag `v2.0.0rc3` → CI builds and uploads
Linux/Windows; run the script for macOS; the Release lists all three downloads.
(`package_macos.sh` without any token just builds `COPRA-macos.zip` for hosting
elsewhere.)

## Architecture

- `copra/core/` — pure NumPy/SciPy computation, no Qt or matplotlib imports,
  unit-testable on its own.
- `copra/plotting/` — matplotlib figures (return `Figure` objects only).
- `copra/gui/` — PySide6/Qt6 GUI; calls only into `core` and `plotting`.
- `copra/cli.py` — command-line entry point (`copra-cli`).

## Development setup

```sh
cd copra_py
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
```

Run the checks:

```sh
ruff check .
ruff format --check .
pytest
```
