Metadata-Version: 2.4
Name: phimaker
Version: 0.5.0
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Rust
Classifier: Topic :: Scientific/Engineering :: Mathematics
License-File: COPYING
Home-Page: https://github.com/tomchaplin/phimaker
Author: Thomas Chaplin, Abhinav Natarajan
License-Expression: GPL-3.0-or-later
Requires-Python: >=3.12
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Repository, https://github.com/tomchaplin/phimaker

<div align="center">

<h1>phimaker</h1>

<b>P</b>ersitent <b>h</b>omology of <b>im</b>ages <b>a</b>nd (co)<b>ker</b>nels.

</div>

## Overview

Phimaker is a Rust library implementing the algorithm introduced in [[1]](#1) for computing persistent homology for kernels, images and cokernels.
Python bindings are provided via PyO3.
The Python package requires Python 3.12 or newer.

See [CHANGELOG.md](CHANGELOG.md) for changes since the last release and API
migration notes.

Install via
```
pip install phimaker
```

## Development environment and source builds

Install [Rust through rustup](https://rustup.rs/),
[uv](https://docs.astral.sh/uv/getting-started/installation/), and
[Lefthook](https://lefthook.dev/installation/). A native C linker is also
required (for example, the build tools for your operating system).

This checkout currently resolves LoPhat 0.14.0 from `../lophat`. Clone
[LoPhat](https://github.com/tomchaplin/lophat) alongside Phimaker and check out
`v0.14.0` or a compatible newer revision before building. See
[the local dependency notes](tests/README.md#local-lophat-014-development).

Run the following commands from the repository root:

```sh
rustup toolchain install stable
rustup toolchain install nightly --component rustfmt
uv python pin 3.12
uv sync --locked --group test
lefthook install
```

uv creates `.venv`, installs the development and test tools, and builds the
Python extension from source. Python 3.12 or newer is required; pin another
supported version if desired. The Git pre-commit hooks run nightly
rustfmt, Tombi, and Ruff, and stage their formatting changes. You can also run
them manually with `lefthook run pre-commit`.

Rebuild the extension and its generated type stubs after Rust or docstring
changes, then run the Python tests:

```sh
uv run --no-sync maturin develop --locked --generate-stubs
uv run --no-sync python -m pytest
```

Build a distributable wheel into `dist/` with:

```sh
uv run --no-sync maturin build --locked --release --generate-stubs --out dist
```

For the Rust library without Python bindings, use:

```sh
cargo build --locked --release --no-default-features
cargo test --locked --no-default-features
```

## Persistence diagrams

`DiagramEnsemble` exposes the diagrams as `domain`, `codomain`, `image`,
`kernel`, `cokernel`, and `relative` in both Rust and Python.

Each diagram is a mapping from a generator's birth index to its death index.
In Rust, the shared `lophat::utils::PersistenceDiagram` wraps a `HashMap<usize, ExtendedUsize>`, with
`ExtendedUsize::Finite(index)` for finite deaths and `ExtendedUsize::Infinity`
for essential classes. Both types are re-exported from `phimaker::diagrams`.
Birth and death indices refer to filtration columns.

In Python, diagrams are dictionaries with integer keys and integer or `None`
values. For example, `diagrams.codomain == {0: None, 1: 2}` describes an essential
class born at index 0 and a class born at index 1 that dies at index 2.
Use `diagram[birth]` to look up a death and `diagram.items()` to iterate over
intervals. The former `.paired` and `.unpaired` attributes have been removed.

`domain_representatives`, `codomain_representatives`, `image_representatives`,
`kernel_representatives`, `cokernel_representatives`, and `relative_representatives`
are optional. Python's `sixpack_from_inclusion` and `sixpack` default to
`get_representatives=False`, which leaves all six attributes as `None` and
skips representative extraction and map solving. Pass `get_representatives=True` to obtain
maps from each birth index to a cycle support, or a relative chain pair.
Dictionary keys identify intervals. For `sixpack_from_inclusion`, all generator
supports use the original codomain indices; domain, kernel, and image cycles
use only selected domain generators. Ordinary non-kernel representatives have
maximum support index equal to birth, as does the second relative component.
Kernel cycles and first relative components use strictly earlier domain indices.

For `sixpack`, keys and diagram endpoints remain cylinder interval identifiers,
but supports refer to the original input complexes:

| Representatives | Support coordinates |
| --- | --- |
| Domain and kernel | Original domain indices |
| Image, codomain, and cokernel | Original codomain indices |
| Relative pair `(a, b)` | Original domain indices in `a`, original codomain indices in `b` |

Image cycles are mapped into the codomain using the input chain map. Cokernel
cycles are interpreted modulo codomain boundaries and images of domain cycles.
Relative pairs satisfy `boundary(a) = 0` and `boundary(b) = f(a)`; their degrees
are one less than, and equal to, the feature's degree. Either component may be
empty, and `b` need not be a codomain cycle. For an inclusion, `a = boundary(b)`,
and `b` uses only generators outside the domain. For the edge relative to its endpoints:

```python
from phimaker import sixpack_from_inclusion

diagrams = sixpack_from_inclusion(
	[[], [], [0, 1]],
	[0, 0, 1],
	[0, 1],
	get_representatives=True,
)
assert diagrams.relative == {2: None}
assert diagrams.relative_representatives == {2: ([0, 1], [2])}
```

Rust stores ordinary representatives as `Option<HashMap<usize, Vec<usize>>>`
and relative pairs as `Option<HashMap<usize, (Vec<usize>, Vec<usize>)>>`. Python
exposes dictionaries of lists or tuples of two lists, or `None`. Each requested map has exactly
its diagram's birth keys. A requested empty diagram has `Some` of an empty map
in Rust and `{}` in Python; when extraction is skipped it still has `None`.

Rust callers supply `get_representatives` immediately after the input matrices
and map or domain indices in `sixpack` and `sixpack_from_inclusion`, or as the
argument to `all_diagrams`. Pass `false` for diagrams only or
`true` for cycles, relative pairs, and maps. Nonempty domain, codomain, and relative decompositions must
retain V only when requesting representatives; the computation entry points
retain these matrices automatically. This flag skips representative extraction, map solving,
and their allocations; it does not change which matrices the reductions retain.

For example, an external edge creates a kernel class at index 4, and a domain
edge kills it at index 5. Its representative is a domain cycle that bounds in
the codomain:

```python
from phimaker import sixpack_from_inclusion

diagrams = sixpack_from_inclusion(
	[[], [], [], [0, 1], [0, 2], [1, 2]],
	[0, 0, 0, 1, 1, 1],
	[1, 2, 5],
	get_representatives=True,
)
assert diagrams.kernel == {4: 5}
assert diagrams.kernel_representatives == {4: [1, 2]}
```

For a cokernel example, let two parallel edges join the same vertices, with only
the second edge in the domain. The codomain loop represents an essential
cokernel feature:

```python
diagrams = sixpack_from_inclusion(
	[[], [], [0, 1], [0, 1]],
	[0, 0, 1, 1],
	[0, 1, 3],
	get_representatives=True,
)
assert diagrams.cokernel == {3: None}
assert diagrams.cokernel_representatives == {3: [2, 3]}
```

## Persistence maps

Setting `get_representatives=True` also computes five maps in the stored cycle
bases: `kernel_to_domain`, `domain_to_image`, `image_to_codomain`,
`domain_to_codomain`, and `codomain_to_cokernel`. Each is
`Option<HashMap<usize, Vec<usize>>>` in Rust and a dictionary or `None` in Python.
All five are `None` when extraction is skipped.

Each dictionary has exactly its source diagram's birth keys. The value is a
sorted list of target births with coefficient one over F2; an empty list denotes
zero. A requested map with an empty source is an empty dictionary. For example,
a younger vertex included in an edge maps to two codomain features, while both
codomain features project to the same cokernel feature:

```python
diagrams = sixpack_from_inclusion(
	[[], [], [0, 1]],
	[0, 0, 1],
	[1],
	get_representatives=True,
)
assert diagrams.kernel_to_domain == {}
assert diagrams.domain_to_image == {1: [1]}
assert diagrams.image_to_codomain == {1: [0, 1]}
assert diagrams.domain_to_codomain == {1: [0, 1]}
assert diagrams.codomain_to_cokernel == {0: [0], 1: [0]}
```

Coefficients use the stored representative bases and identify intervals in
the diagrams, rather than the generator indices in their supports. Every nonzero coefficient satisfies
`target_birth <= source_birth < target_death <= source_death`, with infinite
deaths allowed. At an index, evaluate a map using only source and target
intervals alive there. Compose these evaluated maps; ordinary multiplication of
complete coefficient tables can retain entries between disjoint intervals.
For `sixpack`, use `metadata.times` to evaluate at actual filtration times;
finite intervals with equal birth and death times are omitted from the returned data.

Maps are computed from the complete diagrams, including intervals of zero
duration in actual filtration time. Before returning, `sixpack` removes these
intervals and their representative entries and restricts all map rows and columns
to the remaining intervals. Interval identifiers and cylinder metadata are not
renumbered. Neither diagrams nor representative dictionaries
need to be supplied separately to a map-computation function.

## References

<a id="1">[1]</a>
Cohen-Steiner, D., Edelsbrunner, H., Harer, J. and Morozov, D., 2009, January.
Persistent homology for kernels, images, and cokernels.
In Proceedings of the twentieth annual ACM-SIAM symposium on Discrete algorithms (pp. 1011-1020).
Society for Industrial and Applied Mathematics.

## API documentation

Install the Python documentation dependencies into the project environment,
rebuild the extension, and generate the HTML reference:

```sh
uv sync --locked --group test --group docs
uv run --no-sync maturin develop --locked --generate-stubs
uv run --no-sync python docs/build.py
```

Open `docs/_build/html/index.html` in a browser. Sphinx reads the installed
extension's docstrings, so rebuild the extension after changing them. The build
script treats warnings as errors. The equivalent Sphinx command is:

```sh
uv run --no-sync python -m sphinx -W --keep-going -b html docs docs/_build/html
```

Run `cargo doc --locked --no-default-features --no-deps --open` to build and open
the Rust API reference at `target/doc/phimaker/index.html`. The public
functions document input assumptions and index coordinates, with runnable Rust
examples. Inline `///` comments supply Rust documentation, while separate
feature-gated `#[doc = python_doc!(...)]` attributes supply Python docstrings.
Shared classes retain Rust comments for rustdoc and rust-analyzer; Python builds
strip those comments so only Python usage documentation appears in `help()`.
Use `help(phimaker.sixpack_from_inclusion)` or `help(phimaker.sixpack)` after
building/installing the extension for Python examples.

The inclusion API takes boundary columns, degrees, and domain column indices.
It uses filtration indices only. The general-map API `sixpack` takes timed
complexes and returns mapping-cylinder metadata alongside its diagrams; use
that metadata to interpret the returned indices. Inclusion entry points validate
index ranges, degree-list length, triangularity, degree lowering, and domain
closure, and normalize row supports and domain indices. The boundary must square
to zero; that algebraic condition remains a caller precondition.

Rust `sixpack_from_inclusion` and `sixpack` borrow their inputs and return
`Result<_, InclusionError>`. The decomposition entry points return the same
structured errors. Python exports retain the names `sixpack_from_inclusion` and
`sixpack`; their wrappers release the GIL during computation and convert
structural errors to `ValueError`. Upstream reduction and serialization panics
are not converted to these validation errors.

Boundary supports and domain indices can be in any order, and repeated indices
are treated as repeated listings of the same element, not summed modulo two.
The Rust library is built as an `rlib` alongside the Python extension.

For reduced matrices, use `all_decompositions` or `all_decompositions_slow`.
Their codomain result `d_cod` retains both R and V in original input generator
coordinates. For a finite codomain feature `(birth, death)`, R column `death`
gives its representative cycle; for an essential feature, use V column `birth`.
Both diagram extraction modes read codomain pairs directly in these coordinates.
The relative result `d_rel` retains R and V in local quotient coordinates.
Translate local index `i` to original codomain indices with
`metadata.dom_first_permutation.inverse_map(i + metadata.sz_domain)`.
Relative finite features use R death columns and essential features use V birth
columns. Both reductions preserve the generator order without anti-transposition.
Retaining V adds memory use and increases the slow-mode file sizes.
Python convenience functions expose representative maps on the returned
`DiagramEnsemble` when called with `get_representatives=True`.

## Tests

Run `cargo test --locked` for the pure Rust suite. For Python, build the current
extension and run pytest as described in [tests/README.md](tests/README.md).
That document records each fixture's intent, including slow-mode serialization
regressions and executable documentation examples.

The Cargo dependency uses LoPhat version 0.14.0.
Building with `python-module` also enables `lophat/python-module`, providing
conversion of diagrams to dictionaries and infinite deaths to `None`.

## CI and releases

Every push, pull request, and manual workflow dispatch runs the Rust tests and
builds and tests Python wheels on native Linux and Windows x64/ARM64 runners and
macOS ARM64. The matrix selects stable CPython versions allowed by
`requires-python`, including supported free-threaded variants where available.
Each wheel includes generated type stubs and is installed before pytest runs.
`DiagramEnsemble` and `CylinderMetadata` are importable from `phimaker` for typed
results.

Publishing a GitHub release runs these checks again and publishes the tested
wheels to PyPI and the crate to crates.io. See [docs/releasing.md](docs/releasing.md)
for credentials, platform coverage, and the release procedure.

