Metadata-Version: 2.5
Name: copra2
Version: 2.0.0rc7
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 <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.12
Requires-Dist: matplotlib>=3.7
Requires-Dist: numpy>=1.24
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 general notes on COPRA and differences/ improvements in COPRA2.

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 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 COPRA2. `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, and **File → Open recent** reopens one
of the last ten sessions; **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

# Is the age uncertainty realistic? Leave-one-out check of the dating points;
# suggests whether to switch on the growth variability between datings
copra-cli check-uncertainty --dating DATING.txt --remove-points 3
copra-cli run --dating DATING.txt --proxy PROXY.txt --interp-uncertainty 1

# 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** — `depth`, `age`, `age error` (1σ), one row per dated level.
  An optional upper age error makes the error *asymmetric* (sampled with a
  two-piece normal), and an optional depth error (± distance from the sample
  centre) can go in column 2: 4 columns are either `depth, age, lower, upper`
  or `depth, depth error, age, age error` — recognised automatically (ages are
  near-monotone in depth, depth errors are not) — and 5 columns are
  `depth, depth error, age, lower, upper`. In every Monte Carlo realisation the
  depth of each dating sample is drawn uniformly within ± its depth error.
- **Proxy record** — `depth`, [`sample width`,] `proxy value` [, `value
  error`]. The value error must be 1σ; the sample width is ± half the sampled
  depth interval. Columns are recognised by their content (the value scatters
  most) and can be reassigned in the Proxy table tab (CLI: `--proxy-columns`).
  Without a width column the *Proxy sample width* is None, Fixed ± (half the
  drill diameter) or Continuous (milled, from the sample spacing); CLI:
  `--sample-width 0.5|continuous`. Sample depths are drawn uniformly within
  their interval.
- **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).

**Export** writes the selected result files — any combination; each a
comma-separated `.csv` file with one header line — plus the JSON session and the
run log. Names carry the sample name and the date in ISO `YYYY-MM-DD` form:

- `<sample>_agemodel_ensemble_<date>.csv` — depth, measured proxy value, and one
  age column per Monte Carlo realisation.
- `<sample>_proxy_ensemble_dt<ΔT>_<date>.csv` — the proxy of every realisation
  interpolated onto an equidistant age axis with sampling time ΔT.
- `<sample>_proxy_<interp>_<date>.csv` — proxy average: age, proxy, age and proxy
  confidence limits, depth.
- `<sample>_growthrate_<date>.csv` — depth, age, growth rate and its confidence
  limits.

Central values use the chosen estimate (GUI preference / `--stat`, median by
default, or mean). The field separator is comma (default), semicolon or tab,
chosen in the export dialog. On the CLI choose files with `--outputs`, ΔT with
`--dt` and the separator with `--delimiter`.
- `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. On macOS the runtime selects a padded variant,
`copra/gui/resources/copra_icon_macos.png`, to match the Dock icon's optical
scale.

### 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).

The macOS Dock icon uses a transparent margin around the COPRA artwork, matching
the optical scale of the KalkLicht icon. Its source renditions are in
`packaging/copra.iconset/`; rebuild `packaging/copra.icns` with
`python packaging/rebuild_macos_icon.py` from `copra_py/`.

#### 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/ (builds the app when needed, zips, uploads, and
# attaches the file to the Release of the tag):
packaging/package_macos.sh --all                       # Apple Silicon + Intel
packaging/package_macos.sh                             # Apple Silicon only
packaging/package_macos.sh --intel v2.0.0-rc.7         # Intel only, explicit tag
```

The apps are built by `packaging/build_macos.sh` with python.org's *universal2*
Python 3.12 (install it once from python.org; Rosetta 2 for the Intel build): an
Intel Python running under Rosetta 2 builds a genuine Intel app, in `.venv-x86_64`
→ `dist-x86_64/COPRA.app`; the Apple Silicon app is built in `.venv-arm64` →
`dist/COPRA.app`. python.org's Python also keeps the minimum macOS version low:
a MacPorts/Homebrew Python is built for the macOS of the machine that compiled it
(an app built with it needed macOS 26). `packaging/macos_min_version.sh APP`
prints what an app needs. The files are `COPRA-macos-arm64.zip` and
`COPRA-macos-x86_64.zip`.

The token is read from `$TOKEN` (env), else `$TOKEN_FILE`, else
`~/.config/copra/gitlab_token`. The script reads the GitLab host and namespace /
project path from the `origin` remote, then resolves the numeric project ID via
the GitLab Projects API. Set `PROJECT_ID` to bypass lookup, or set
`GITLAB_PROJECT_PATH` and `GITLAB_HOST` when uploading to a different project.
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-<arch>.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
```
