Metadata-Version: 2.4
Name: phimaker
Version: 0.4.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 the generator indices whose sum over F2 gives a cycle.
Keys and supports use the diagram coordinates. Domain, codomain, image, cokernel, and relative cycles
have maximum support index equal to the feature's birth. Kernel cycles contain
only domain generators with indices strictly less than their kernel birth; these
cycles are boundaries in the codomain throughout their kernel intervals.
Cokernel representatives are codomain cycles interpreted modulo codomain
boundaries and domain cycles. A finite representative becomes zero in that
quotient at its death, without necessarily becoming a codomain boundary.
Relative representatives contain only generators outside the domain. Their
codomain boundary lies in the domain, so they are cycles in the quotient even
when they are not codomain cycles. Their classes are interpreted modulo codomain
boundaries and domain chains. For example, when the domain consists of both
vertices of an edge, the edge alone represents an essential relative H1 class:

```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: [2]}
```

Rust stores these as `Option<HashMap<usize, Vec<usize>>>`;
Python exposes dictionaries of 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 and maps. Nonempty domain, codomain, image, 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 the same indices as the
diagrams. 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;
equal-time cylinder intervals are retained in the returned data.

Maps are computed from the complete diagrams, including intervals of zero
duration in actual filtration time. Downstream filtering can restrict the
resulting rows and columns. 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.

