diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..30257b2 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Create virtualenv + run: python -m venv .venv + + - name: Add venv to PATH + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + echo "$GITHUB_WORKSPACE/.venv/Scripts" >> $GITHUB_PATH + else + echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH + fi + shell: bash + + - name: Build and install (dev mode) + uses: PyO3/maturin-action@v1 + env: + VIRTUAL_ENV: ${{ github.workspace }}/.venv + with: + command: develop + args: --release + + - name: Install test dependencies + run: pip install pytest + + - name: Run tests + run: pytest tests/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b509749 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +name: Release + +on: + push: + tags: + - "v*" + +jobs: + linux: + name: Build Linux (${{ matrix.target }}) + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + - uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist --find-interpreter + - uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.target }} + path: dist + + windows: + name: Build Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: PyO3/maturin-action@v1 + with: + args: --release --out dist --find-interpreter + - uses: actions/upload-artifact@v4 + with: + name: wheels-windows + path: dist + + macos: + name: Build macOS (${{ matrix.target }}) + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + - uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + - uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.target }} + path: dist + + sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [linux, windows, macos, sdist] + environment: + name: pypi + url: https://pypi.org/p/prinpy + permissions: + id-token: write # required for PyPI trusted publishing (no API token needed) + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index dd8652c..fd6807d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ __pycache__/ build/ dist/ prinpy.egg-info/ +venv/ +target/ +prinpy/_rs/*.pyd +prinpy/_rs/*.pdb +.mypy_cache/ +.pytest_cache/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b991293 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,251 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numpy" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "pyo3-build-config", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "prinpy_rs" +version = "0.1.0" +dependencies = [ + "ndarray", + "numpy", + "pyo3", + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0f80c90 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "prinpy_rs" +version = "0.1.0" +edition = "2024" + +[lib] +name = "prinpy_rs" +crate-type = ["cdylib"] + +[dependencies] +ndarray = "0.17" +pyo3 = "0.28.3" +numpy = "0.28" +thiserror = "1.0" \ No newline at end of file diff --git a/README.md b/README.md index 7a3281a..65700cc 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,191 @@ [![Downloads](https://pepy.tech/badge/prinpy)](https://pepy.tech/project/prinpy) +[![PyPI](https://img.shields.io/pypi/v/prinpy)](https://pypi.org/project/prinpy/) +[![Python](https://img.shields.io/pypi/pyversions/prinpy)](https://pypi.org/project/prinpy/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + # prinPy -`pip install prinpy`
-
-Inspired by [this R package](https://github.com/rcannood/princurve), prinPy brings principal curves to Python. -## What prinPy does -PrinPy has local and global algorithms for computing principal curves. +A high-performance Python library for fitting **principal curves** to n-dimensional data, with core algorithms implemented in Rust for speed. + +Inspired by the [princurve R package](https://github.com/rcannood/princurve). + +--- + +## Installation + +```bash +pip install prinpy +``` + +For neural-network-based fitting (requires PyTorch): + +```bash +pip install "prinpy[neural]" +``` + +**Requirements:** Python ≥ 3.9, NumPy ≥ 1.20 + +--- + +## Quick Start + +```python +import numpy as np +from prinpy.local_curves import ConstrainedFitter, GreedyFit + +# Noisy 2D spiral +theta = np.linspace(0, 3 * np.pi, 400) +r = np.linspace(0, 1, 400) ** 0.5 +data = np.column_stack([r * np.cos(theta), r * np.sin(theta)]) +data += np.random.normal(scale=0.02, size=data.shape) + +# Fit a principal curve +curve = ConstrainedFitter(algorithm=GreedyFit(), tolerance=0.05).fit(data) + +# Project data onto the curve — returns arc lengths, unit positions, and coordinates +projection = curve.project(data) +print(projection.arc_lengths) # distance along the curve for each point +print(projection.unit_lengths) # normalised position in [0, 1] +print(projection.points) # nearest point on the curve + +# Reconstruct 100 evenly-spaced points along the curve +reconstructed = curve.interpolate_from_unit(np.linspace(0, 1, 100)).points +``` + +--- ## What is a Principal Curve? -A principal curve is a smooth n-dimensional curve that passes through the middle of a dataset. Principal curves are a dimensionality reduction tool analogous to a nonlinear principal component. PCs have uses in GPS data, image recognition, bioinformatics, and so much more. -### Local Algorithms -Local algorithms work on a step-by-step basis. Starting at one end of the curve, it will attempt to make segments that meet an acceptable error threshold as it moves from one end of the curve to the other. Once the algorithm can connect the current point to the end point, the algorithm terminates and a curve is interpolated through the segments. PrinPy currently has two local algorithms: +A principal curve is a smooth, one-dimensional manifold that passes through the middle of a dataset. It is the nonlinear generalisation of a principal component — instead of a straight line of best fit, it is a curve of best fit. + +Principal curves are used in GPS track smoothing, bioinformatics, image processing, and anywhere a dataset has an intrinsic one-dimensional structure. + +--- + +## Algorithms + +| Class | Module | Strategy | Best for | +|---|---|---|---| +| `GreedyFit` | `prinpy.local_curves` | CLPC-g (greedy) | Fast fitting, simple or tightly-bunched curves | +| `SVDFit` | `prinpy.local_curves` | CLPC-s (truncated SVD) | Higher accuracy on complex curves | +| `NetworkFitter` | `prinpy.global_curves` | NLPCA (autoencoder) | Sparse data or diffuse point clouds | + +All algorithms return a `PrincipalCurve` with the same interface — your downstream code never depends on which algorithm was used. + +--- + +## Local Algorithms + +Local algorithms grow the curve one segment at a time, marching from one end of the data to the other. They are fast and work well for tightly structured data. + +Both are accessed through `ConstrainedFitter`, which wraps the chosen segment-finding strategy and fits a smooth spline through the resulting vertices. + +```python +from prinpy.local_curves import ConstrainedFitter, GreedyFit, SVDFit + +# Greedy — faster, good for most use cases +curve = ConstrainedFitter(algorithm=GreedyFit(inner_radius=0.9), tolerance=0.05).fit(data) + +# SVD — more accurate for complex or curved shapes +curve = ConstrainedFitter(algorithm=SVDFit(), tolerance=0.05).fit(data) +``` + +**`tolerance`** controls the maximum allowed local fitting error per segment. Lower values produce more control points and a tighter fit; higher values produce a coarser, smoother curve. -1. CLPC-g (Greedy Constraint Local Principal Curve)1 -2. CLPC-s (One-Dimensional Search Constraint Local Principal Curve)1 +--- + +## Global Algorithm (Neural Network) + +The global algorithm fits an autoassociative neural network (NLPCA) whose bottleneck layer encodes the one-dimensional position along the curve. It is better suited to sparse or cloud-like data where local structure is not well-defined. + +```python +from prinpy.global_curves import NetworkFitter, TrainingCallback -CLPC-g will be faster and is fine for simpler curves. CLPS-s has the potential to be much more accurate at the expense of speed for more difficult curves. After fitting a curve, prinPy has the ability to project to the curve. +curve = NetworkFitter( + dim=2, # dimensionality of your data + n_hidden=16, # hidden layer size + lr=0.01, # learning rate + epochs=500, + callback=TrainingCallback(print_progress=True, every_n_epochs=50), +).fit(data) +``` -### Global Algorithms -Global algorithms, unlike local algorithms, are more like minimization problems. Given a dataset, a global algorithm might make an initial guess at a principal curve and adjust it from there. +Requires `pip install "prinpy[neural]"`. -The sole global algorithm as of now performs nonlinear principal component analysis. The global algorithm, called NLPCA in this package, is a neural network implementation.2 This algorithm works by creating an autoassociative neural network with a "bottle-neck" layer which forces the network to learn the most important features of the data. +--- -**Which one should I use?**
-The local algorithms will be better for tightly bunched data, such as digit recogniition or GPS data. The global algorithm is better suited for "clouds" of data or sparsely represented data. +## Working with a Fitted Curve -## Quick-Start -View the quickstart notebook [here](https://github.com/artusoma/prinPy/blob/master/prinPy%20quickstart.ipynb). Docs will be coming soon! +Every algorithm returns a `PrincipalCurve` with the same interface: ```python -# Example of local PC fitting -cl = CLPCG() # Create solver - -# CLPCG.fit() fits the principal curve. takes x_data, y_data, -# and the min allowed error for each step. e_min is acheived -# through trial and error, but 1/4 to 1/2 data error is what authors -# recommend. -cl.fit(xdata, ydata, e_max = .1) -cl.plot() # plots curve, optional axes can be passed - -# Reconstruct curve -tcks = cl.spline_ticks # get spline ticks -xy = scipy.interpolate.splev(np.linspace(0,1,100), self.spline_ticks) +# Total arc length of the curve +total_length = curve.length() + +# Project arbitrary points onto the curve +proj = curve.project(new_data) +proj.points # (n, d) — nearest points on the curve +proj.arc_lengths # (n,) — distance from the start of the curve +proj.unit_lengths # (n,) — normalised position in [0, 1] + +# Interpolate from arc length +proj = curve.interpolate_from_length(np.array([0.0, 0.5, 1.2])) + +# Interpolate from normalised position +proj = curve.interpolate_from_unit(np.linspace(0, 1, 200)) + +# Control points that define the curve's shape +pts = curve.control_points() # (k, d) ``` +--- + +## Development + +prinPy uses [maturin](https://github.com/PyO3/maturin) to build the Rust extension. + +```bash +# Clone and set up +git clone https://github.com/artusoma/prinpy +cd prinpy + +# Install maturin and build the Rust extension in-place +pip install maturin +maturin develop + +# Install Python dependencies (add [neural] for PyTorch support) +pip install -e ".[neural]" + +# Run tests +python -m pytest tests/ +``` + +--- + +## Migrating from v0.x + +v1.0.0 is **not backwards-compatible**. Key changes: + +- A standard `PrincipalCurve` / `CurveFitter` interface now exists — v0.x had no common API +- Core algorithms rewritten in Rust (~70× faster) +- PyTorch replaces Keras/TensorFlow for the neural fitter +- SVDFit replaces the old one-dimensional search algorithm +- All fitters now return a standard `PrincipalCurve` with a unified projection and interpolation API + +--- + ## References -\[1\] Dewang Chen, Jiateng Yin, Shiying Yang, Lingxi Li, Peter Pudney, -Constraint local principal curve: Concept, algorithms and applications, -Journal of Computational and Applied Mathematics, -Volume 298, -2016, -Pages 222-235, -ISSN 0377-0427, -https://doi.org/10.1016/j.cam.2015.11.041. - -\[2\] Mark Kramer, Nonlinear Principal Component Analysis Using -Autoassociative Neural Networks + +[1] Dewang Chen, Jiateng Yin, Shiying Yang, Lingxi Li, Peter Pudney, +*Constraint local principal curve: Concept, algorithms and applications*, +Journal of Computational and Applied Mathematics, Volume 298, 2016, Pages 222–235. +https://doi.org/10.1016/j.cam.2015.11.041 + +[2] Mark Kramer, *Nonlinear Principal Component Analysis Using Autoassociative Neural Networks*, AIChE Journal, 1991. + +--- + +## License + +MIT © [Matthew Artuso](https://github.com/artusoma). See [LICENSE](LICENSE) for details. + diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 2f7efbe..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-minimal \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index f99a524..0000000 --- a/docs/index.md +++ /dev/null @@ -1,4 +0,0 @@ -# prinPy Documentation -Welcome to the prinPy documentation! - -This site is under construction. diff --git a/examples/_test_data.py b/examples/_test_data.py new file mode 100644 index 0000000..738211c --- /dev/null +++ b/examples/_test_data.py @@ -0,0 +1,105 @@ +import numpy as np +from numpy.typing import NDArray +from prinpy.interfaces import PrincipalCurve +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +from matplotlib.axes import Axes +import seaborn as sns + +sns.set_style("whitegrid") + + +def plot_2d_data( + axes: Axes, + data: NDArray, + curve: PrincipalCurve, + title: str = "2D Curve Fitting", +): + reconstructed = curve.interpolate_from_unit(np.linspace(0, 1, 100)).points + + axes.scatter(x=data[:, 0], y=data[:, 1], s=10, alpha=0.5) + axes.plot(reconstructed[:, 0], reconstructed[:, 1], alpha=0.75, c="C1") + axes.scatter( + x=curve.control_points()[:, 0], + y=curve.control_points()[:, 1], + s=20, + alpha=0.5, + c="C1", + ) + axes.set_title(title) + axes.grid(False) + + +def plot_3d_data( + ax3d: Axes3D, + data: NDArray, + curve: PrincipalCurve, + title: str = "3D Curve Fitting", +): + reconstructed = curve.interpolate_from_unit(np.linspace(0, 1, 100)).points + + ax3d.scatter( + xs=data[:, 0], + ys=data[:, 1], + zs=data[:, 2], # type: ignore + s=10, + alpha=0.5, + c="C0", + ) + ax3d.plot( + reconstructed[:, 0], + reconstructed[:, 1], + reconstructed[:, 2], # type: ignore + alpha=0.5, + c="C1", + ) + ax3d.scatter( + xs=curve.control_points()[:, 0], + ys=curve.control_points()[:, 1], + zs=curve.control_points()[:, 2], # type: ignore + s=20, + alpha=0.5, + c="C1", + ) + ax3d.set_title(title) + + +def create_spiral(n_points: int = 500, noise: float = 0.02) -> NDArray: + theta = np.linspace(0, np.pi * 3, n_points) + r = np.linspace(0, 1, n_points) ** 0.5 + + x_data = r * np.cos(theta) + np.random.normal(scale=noise, size=n_points) + y_data = r * np.sin(theta) + np.random.normal(scale=noise, size=n_points) + data = np.column_stack([x_data, y_data]) + return data + + +def create_parabola(n_points: int = 500, noise: float = 0.025) -> NDArray: + t = np.linspace(-1, 1, n_points) + x = t + y = t**2 + pts = np.column_stack([x, y]) + pts += np.random.default_rng(0).normal(scale=noise, size=pts.shape) + return pts + + +def create_3d_parabola(n_points: int = 300, noise: float = 0.025) -> NDArray: + t = np.linspace(-1, 1, n_points) + x = t + y = t**2 + z = t**3 + pts = np.column_stack([x, y, z]) + pts += np.random.default_rng(0).normal(scale=noise, size=pts.shape) + return pts + + +def create_3d_spiral(n_points: int = 300, noise: float = 0.025) -> NDArray: + theta = np.linspace(0, np.pi * 3, n_points) + r = np.linspace(0, 1, n_points) ** 0.5 + + x_data = r * np.cos(theta) + np.random.normal(scale=noise, size=n_points) + y_data = r * np.sin(theta) + np.random.normal(scale=noise, size=n_points) + z_data = r + np.random.normal(scale=noise, size=n_points) + + data = np.column_stack([x_data, y_data, z_data]) + return data diff --git a/examples/figures/3d_curve_fitting.png b/examples/figures/3d_curve_fitting.png new file mode 100644 index 0000000..f2a9edf Binary files /dev/null and b/examples/figures/3d_curve_fitting.png differ diff --git a/examples/figures/3d_network_fitting.png b/examples/figures/3d_network_fitting.png new file mode 100644 index 0000000..9b4323d Binary files /dev/null and b/examples/figures/3d_network_fitting.png differ diff --git a/examples/figures/Parabola_curve_fitting.png b/examples/figures/Parabola_curve_fitting.png new file mode 100644 index 0000000..6717531 Binary files /dev/null and b/examples/figures/Parabola_curve_fitting.png differ diff --git a/examples/figures/Parabola_network_fitting.png b/examples/figures/Parabola_network_fitting.png new file mode 100644 index 0000000..3abd839 Binary files /dev/null and b/examples/figures/Parabola_network_fitting.png differ diff --git a/examples/figures/Spiral_curve_fitting.png b/examples/figures/Spiral_curve_fitting.png new file mode 100644 index 0000000..50bbd03 Binary files /dev/null and b/examples/figures/Spiral_curve_fitting.png differ diff --git a/examples/figures/Spiral_network_fitting.png b/examples/figures/Spiral_network_fitting.png new file mode 100644 index 0000000..ad77038 Binary files /dev/null and b/examples/figures/Spiral_network_fitting.png differ diff --git a/examples/global_fitters.py b/examples/global_fitters.py new file mode 100644 index 0000000..5a04350 --- /dev/null +++ b/examples/global_fitters.py @@ -0,0 +1,64 @@ +from pathlib import Path + +from prinpy.global_curves import NetworkFitter, TrainingCallback +import matplotlib.pyplot as plt + +from _test_data import * + +_FIGURES = Path(__file__).parent / "figures" + + +def main(): + _FIGURES.mkdir(exist_ok=True) + + data_generators_2d = { + "Spiral": create_spiral, + "Parabola": create_parabola, + } + n_hidden_values = [8, 16, 32] + + for name, generator in data_generators_2d.items(): + data = generator() + fig, ax = plt.subplots(1, 3, figsize=(12, 4)) + for idx, n_hidden in enumerate(n_hidden_values): + curve = NetworkFitter( + dim=2, + n_hidden=n_hidden, + lr=0.01, + epochs=500, + callback=TrainingCallback(print_progress=False), + ).fit(data) + plot_2d_data( + ax[idx], + data, + curve, + title=f"NetworkFitter — n_hidden={n_hidden}", + ) + fig.tight_layout() + fig.savefig(_FIGURES / f"{name}_network_fitting.png") + + data_generators_3d = { + "3D Spiral": create_3d_spiral, + "3D Parabola": create_3d_parabola, + } + n_hidden_values_3d = [16, 32] + + fig = plt.figure(figsize=(10, 8)) + for col, n_hidden in enumerate(n_hidden_values_3d): + for row, (shape_name, generator) in enumerate(data_generators_3d.items()): + data = generator() + ax3d = fig.add_subplot(2, 2, row * 2 + col + 1, projection="3d") + curve = NetworkFitter( + dim=3, + n_hidden=n_hidden, + lr=0.01, + epochs=500, + callback=TrainingCallback(print_progress=False), + ).fit(data) + plot_3d_data(ax3d, data, curve, title=f"{shape_name} — n_hidden={n_hidden}") + fig.tight_layout() + fig.savefig(_FIGURES / "3d_network_fitting.png") + + +if __name__ == "__main__": + main() diff --git a/examples/local_fitters.py b/examples/local_fitters.py new file mode 100644 index 0000000..0a765d1 --- /dev/null +++ b/examples/local_fitters.py @@ -0,0 +1,65 @@ +from pathlib import Path +from prinpy.local_curves import ConstrainedFitter, GreedyFit, SVDFit +import numpy as np +import matplotlib.pyplot as plt +from numpy.typing import NDArray +from _test_data import * + +_FIGURES = Path(__file__).parent / Path("figures") + +def main(): + data_generators = { + "Spiral": create_spiral, + "Parabola": create_parabola, + } + for name, generator in data_generators.items(): + data = generator() + fig, ax = plt.subplots(2, 3, figsize=(9, 6)) + for idx, tolerance in enumerate([0.03, .075, .25]): + curve = ConstrainedFitter( + algorithm=GreedyFit(inner_radius=0.9), + tolerance=tolerance, + ).fit(data) + plot_2d_data( + ax[0, idx], + data, + curve, + title=f"GreedyFit — tolerance={tolerance}", + ) + + curve = ConstrainedFitter( + algorithm=SVDFit(), + tolerance=tolerance, + ).fit(data) + plot_2d_data( + ax[1, idx], + data, + curve, + title=f"SVDFit — tolerance={tolerance}", + ) + fig.tight_layout() + fig.savefig(_FIGURES / f"{name}_curve_fitting.png") + + data_generators_3d = { + "3D Spiral": create_3d_spiral, + "3D Parabola": create_3d_parabola, + } + algorithms = { + "GreedyFit": lambda: GreedyFit(inner_radius=0.9), + "SVDFit": SVDFit, + } + tolerance = 0.05 + + fig = plt.figure(figsize=(10, 8)) + for col, (shape_name, generator) in enumerate(data_generators_3d.items()): + data = generator() + for row, (algo_name, algo_cls) in enumerate(algorithms.items()): + ax3d = fig.add_subplot(2, 2, row * 2 + col + 1, projection="3d") + curve = ConstrainedFitter(algorithm=algo_cls(), tolerance=tolerance).fit(data) + plot_3d_data(ax3d, data, curve, title=f"{shape_name} — {algo_name}") + fig.tight_layout() + fig.savefig(_FIGURES / "3d_curve_fitting.png") + + +if __name__ == "__main__": + main() diff --git a/examples/quickstart.py b/examples/quickstart.py new file mode 100644 index 0000000..97dcda2 --- /dev/null +++ b/examples/quickstart.py @@ -0,0 +1,36 @@ +import numpy as np +from prinpy.local_curves import ConstrainedFitter, GreedyFit + +# Noisy 2D spiral +theta = np.linspace(0, 3 * np.pi, 400) +r = np.linspace(0, 1, 400) ** 0.5 +data = np.column_stack([r * np.cos(theta), r * np.sin(theta)]) +data += np.random.normal(scale=0.02, size=data.shape) + +# Fit a principal curve +curve = ConstrainedFitter(algorithm=GreedyFit(), tolerance=0.05).fit(data) + +# Project data onto the curve — returns arc lengths, unit positions, and coordinates +projection = curve.project(data) +print(projection.arc_lengths) # distance along the curve for each point +print(projection.unit_lengths) # normalised position in [0, 1] +print(projection.points) # nearest point on the curve + +# Reconstruct 100 evenly-spaced points along the curve +reconstructed = curve.interpolate_from_unit(np.linspace(0, 1, 100)).points + +# Plotting +import matplotlib.pyplot as plt +plt.scatter(data[:, 0], data[:, 1], s=10, alpha=0.5, c="C0", label="Data") +plt.plot(reconstructed[:, 0], reconstructed[:, 1], alpha=0.5, c="C1", label="Curve") +plt.scatter( + x=curve.control_points()[:, 0], + y=curve.control_points()[:, 1], + s=20, + alpha=0.5, + c="C1", + label="Control Points", +) +plt.legend() +plt.show() +input("Press Enter to exit...") \ No newline at end of file diff --git a/prinPy quickstart.ipynb b/prinPy quickstart.ipynb deleted file mode 100644 index 446a2f1..0000000 --- a/prinPy quickstart.ipynb +++ /dev/null @@ -1,342 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# prinPy Quickstart Guide" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "prinPy has global and local algorithms. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Local Algorithms" - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "metadata": {}, - "outputs": [], - "source": [ - "from prinpy.local import CLPCG\n", - "\n", - "# Some other modules\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns; sns.set()\n", - "import timeit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Generate Test Data" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "theta = np.linspace(0,np.pi*3, 1000)\n", - "r = np.linspace(0,1,1000) ** .5\n", - "\n", - "x_data = r * np.cos(theta) + np.random.normal(scale = .02, size = 1000)\n", - "y_data = r * np.sin(theta) + np.random.normal(scale = .02, size = 1000)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Plot" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(x_data, y_data, s = 1)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Fit Principal Curve with Local Algorithms" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Took 0.214514 seconds\n" - ] - } - ], - "source": [ - "cl = CLPCG() # Create CLPCG object\n", - "\n", - "# the fit() method calculates the principal curve\n", - "# e_max is determined through trial and error as of\n", - "# now, but aim for about 1/2 data error and adjust from\n", - "# there. \n", - "start = timeit.default_timer()\n", - "\n", - "cl.fit(x_data, y_data, e_max = .03) # CLPCG.fit() to fit PC\n", - "\n", - "stop = timeit.default_timer()\n", - "\n", - "print(\"Took %f seconds\" % (stop - start))" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 76, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "ax.scatter(x_data, y_data, s = 3, alpha = .7)\n", - "cl.plot(ax) # .plot will display the fit curve.\n", - " # you can optionally pass in a matplotlib ax\n", - "pts = cl.fit_points # fitted points with PC that spline is passed through\n", - "ax.scatter(pts[:,0], pts[:,1], s = 40, c = 'green')" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# .proj will return a projection index for each point\n", - "proj = cl.project(x_data, y_data) \n", - "print(proj[:5])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0. 0. 0. 0. 0.02700805 0.04754636\n", - " 0.09041594 0.12945787 0.18449046 0.22115265 0.2495283 0.30794099\n", - " 0.36219516 0.4034585 0.45169821 0.50868772 0.57010873 0.63433081\n", - " 0.69801242 0.75620144 0.80715538 0.89094181 1. 1.\n", - " 1. 1. ]\n", - "[[ 0.01447173 0.01714681]\n", - " [ 0.06537318 -0.00911538]\n", - " [ 0.17175084 0.04210127]\n", - " [ 0.21407512 0.16854487]\n", - " [ 0.05238058 0.39507542]]\n" - ] - } - ], - "source": [ - "# additionally, you can get spline ticks or fit points:\n", - "tck = cl.spline_ticks\n", - "fit_pts = cl.fit_points\n", - "\n", - "print(tck[0])\n", - "print(fit_pts[:5])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Global" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "# NLPCA is the global alg\n", - "from prinpy.glob import NLPCA" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "metadata": {}, - "outputs": [], - "source": [ - "# Generate some test data\n", - "t = np.linspace(0, 1, 1000) + np.random.normal(scale = .1, size = 1000)\n", - "x = 5*np.cos(t) + np.random.normal(scale = .1, size = 1000)\n", - "y = np.sin(t) + np.random.normal(scale = .1, size = 1000)" - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 77, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(x, y, s = 1)" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "metadata": {}, - "outputs": [], - "source": [ - "# create solver\n", - "pca = NLPCA()\n", - "\n", - "# transform data for better training with the \n", - "# neural net using built in preprocessor\n", - "data_new = pca.preprocess( [x,y] )\n", - "\n", - "# fit the data\n", - "pca.fit(data_new, epochs = 150, nodes = 15, lr = .01, verbose = 0)\n", - "\n", - "# project the current data. This returns a projection\n", - "# index for each point and points to plot the curve\n", - "proj, curve_pts = pca.project(data_new)" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 81, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(data_new[:,0], \n", - " data_new[:,1], \n", - " s = 5, \n", - " c = proj.reshape(-1), \n", - " cmap = 'viridis')\n", - "plt.plot(curve_pts[:,0], \n", - " curve_pts[:,1], \n", - " color = 'black',\n", - " linewidth = '2.5')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/prinpy/_containers.py b/prinpy/_containers.py new file mode 100644 index 0000000..d9c16a8 --- /dev/null +++ b/prinpy/_containers.py @@ -0,0 +1,195 @@ +from prinpy.interfaces import PrincipalCurve, Projection +import numpy as np +import scipy.interpolate as si +from prinpy._rs import find_nearest_points +from scipy.integrate import quad +from prinpy._utils import _check_shape +from dataclasses import dataclass + +@dataclass(frozen=True) +class _Segment: + start: np.ndarray + end: np.ndarray + + def length(self) -> float: + return float(np.linalg.norm(self.end - self.start)) + +class _SegmentCurve(PrincipalCurve): + """ + Special case of spline curve where number of points is less than 4. + Connects control points via straight line segments. + + Args: + control_points (np.ndarray): The control points of the segment curve. + """ + + def __init__(self, control_points: np.ndarray): + self._control_points = control_points + diffs = np.diff(control_points, axis=0) + self._segment_lengths = np.linalg.norm(diffs, axis=1) + self._cumulative_lengths = np.concatenate([[0.0], np.cumsum(self._segment_lengths)]) + self._arc_length = float(self._cumulative_lengths[-1]) + + def control_points(self) -> np.ndarray: + return self._control_points + + def length(self) -> float: + return self._arc_length + + @_check_shape(2) + def project(self, X: np.ndarray) -> Projection: + pts = self._control_points + best_points = np.empty_like(X) + best_arc_lengths = np.empty(len(X)) + best_dists_sq = np.full(len(X), np.inf) + + for i in range(len(pts) - 1): + a, b = pts[i], pts[i + 1] + ab = b - a + seg_len_sq = float(np.dot(ab, ab)) + + if seg_len_sq == 0.0: + t = np.zeros(len(X)) + proj = np.tile(a, (len(X), 1)) + else: + t = np.clip(((X - a) @ ab) / seg_len_sq, 0.0, 1.0) + proj = a + t[:, None] * ab + + dists_sq = np.sum((X - proj) ** 2, axis=1) + mask = dists_sq < best_dists_sq + best_points[mask] = proj[mask] + best_dists_sq[mask] = dists_sq[mask] + best_arc_lengths[mask] = self._cumulative_lengths[i] + t[mask] * self._segment_lengths[i] + + return Projection( + points=best_points, + arc_lengths=best_arc_lengths, + unit_lengths=best_arc_lengths / self._arc_length, + ) + + @_check_shape(1) + def interpolate_from_length(self, X: np.ndarray) -> Projection: + X = np.clip(X, 0.0, self._arc_length) + idxs = np.clip( + np.searchsorted(self._cumulative_lengths, X, side="right") - 1, + 0, len(self._segment_lengths) - 1, + ) + local_t = np.clip( + (X - self._cumulative_lengths[idxs]) / self._segment_lengths[idxs], 0.0, 1.0 + ) + a = self._control_points[idxs] + b = self._control_points[idxs + 1] + curve_points = a + local_t[:, None] * (b - a) + return Projection( + points=curve_points, + arc_lengths=X, + unit_lengths=X / self._arc_length, + ) + + @_check_shape(1) + def interpolate_from_unit(self, X: np.ndarray) -> Projection: + arc_lengths = np.clip(X, 0.0, 1.0) * self._arc_length + idxs = np.clip( + np.searchsorted(self._cumulative_lengths, arc_lengths, side="right") - 1, + 0, len(self._segment_lengths) - 1, + ) + local_t = np.clip( + (arc_lengths - self._cumulative_lengths[idxs]) / self._segment_lengths[idxs], 0.0, 1.0 + ) + a = self._control_points[idxs] + b = self._control_points[idxs + 1] + curve_points = a + local_t[:, None] * (b - a) + return Projection( + points=curve_points, + arc_lengths=arc_lengths, + unit_lengths=X, + ) + + + + +class _SplineCurve(PrincipalCurve): + """ + A class representing a spline curve. + + Args: + control_points (np.ndarray): The control points of the spline curve. + """ + def __init__(self, control_points: np.ndarray): + self._spline = si.make_splprep(control_points.T, s=0)[0] + self._arc_length = self._calculate_arc_length() + self._control_points = control_points + + def control_points(self) -> np.ndarray: + return self._control_points + + @_check_shape(2) + def project(self, X: np.ndarray, sample_resolution: int = 500) -> Projection: + X = np.ascontiguousarray(X, dtype=np.float32) + samples = np.linspace(0, 1, sample_resolution) + curve_points = np.array(self._spline(samples)).T + curve_points = np.ascontiguousarray(curve_points, dtype=np.float32) + point_idxs = find_nearest_points(X, curve_points) + + # Get points and arc-lengths + points = curve_points[point_idxs] + lengths = samples[point_idxs] * self.length() + unit_lengths = samples[point_idxs] + + return Projection(points=points, arc_lengths=lengths, unit_lengths=unit_lengths) + + @_check_shape(1) + def interpolate_from_length(self, X: np.ndarray) -> Projection: + """Return the projection of the input points onto the curve. + + Args: + X (np.ndarray): A 1D array of arc-lengths. + + Returns: + Projection: A Projection object containing the projected points and their corresponding + arc-lengths. + """ + curve_points = np.array(self._spline(X / self.length())).T + return Projection( + points=curve_points, arc_lengths=X, unit_lengths=X / self.length() + ) + + @_check_shape(1) + def interpolate_from_unit(self, X: np.ndarray) -> Projection: + """Return the projection of the input points onto the curve. + + Args: + X (np.ndarray): A 1D array of unit-lengths. + + Returns: + Projection: A Projection object containing the projected points and their corresponding + unit-lengths. + """ + curve_points = np.array(self._spline(X)).T + return Projection( + points=curve_points, arc_lengths=X * self.length(), unit_lengths=X + ) + + def length(self) -> float: + return self._arc_length + + def _calculate_arc_length(self) -> float: + """Calculate the arc length of the spline curve using numerical integration. + + Args: + samples (np.ndarray): Sample points along the curve. + Has shape (n_samples, n_dimensions). + + Returns: + float: The arc length of the curve. + """ + + def integrand(t): + dx_dt = self._spline(t, nu=1) + return np.linalg.norm(dx_dt) + + k = self._spline.k + interior_knots = self._spline.t[k + 1 : -(k + 1)] + interior_knots = interior_knots[(interior_knots > 0.0) & (interior_knots < 1.0)] + arc_length, _ = quad(integrand, 0, 1, points=interior_knots) + return arc_length diff --git a/prinpy/_rs/__init__.py b/prinpy/_rs/__init__.py new file mode 100644 index 0000000..dbf9db6 --- /dev/null +++ b/prinpy/_rs/__init__.py @@ -0,0 +1,7 @@ +""" +Python entry point into compiled prinpy Rust binaries +""" + +from .prinpy_rs import clpg, clppca, find_nearest_points + +__all__ = ["clpg", "clppca" , "find_nearest_points"] diff --git a/prinpy/_rs/prinpy_rs.pyi b/prinpy/_rs/prinpy_rs.pyi new file mode 100644 index 0000000..dda6aa6 --- /dev/null +++ b/prinpy/_rs/prinpy_rs.pyi @@ -0,0 +1,6 @@ +import numpy as np + +def clpg(x: np.ndarray, tol: float, inner_radius: float) -> np.ndarray: ... +def clppca(x: np.ndarray, tol: float) -> np.ndarray: ... + +def find_nearest_points(data: np.ndarray, curve: np.ndarray) -> np.ndarray: ... \ No newline at end of file diff --git a/docs/Global.md b/prinpy/_rs/py.typed similarity index 100% rename from docs/Global.md rename to prinpy/_rs/py.typed diff --git a/prinpy/_utils.py b/prinpy/_utils.py new file mode 100644 index 0000000..050596d --- /dev/null +++ b/prinpy/_utils.py @@ -0,0 +1,22 @@ +from functools import wraps +import numpy as np + + +def _check_shape(expected_dim: int): + """A decorator to check the shape of the input data. It checks if the input + data has the expected number of dimensions, and raises a ValueError if it + does not. + """ + + def _dec(f): + @wraps(f) + def _do_check(self, X: np.ndarray, *args, **kwargs): + if X.ndim != expected_dim: + raise ValueError( + f"Expected input to have {expected_dim} dimensions, but got {X.ndim}." + ) + return f(self, X, *args, **kwargs) + + return _do_check + + return _dec diff --git a/prinpy/glob.py b/prinpy/glob.py deleted file mode 100644 index ad7e7cf..0000000 --- a/prinpy/glob.py +++ /dev/null @@ -1,154 +0,0 @@ -''' -This is the global module that contains principal curve and nonlinear -principal component analysis algorithms that work to optimize a line -over an entire dataset. Additionally, these algorithms should work -in space greater than 2-dimensions. -''' - -# General libraries -import numpy as np - -# ML libraries -import tensorflow as tf -import keras -from keras.models import Model -from keras.layers import Dense, Input, LeakyReLU -from keras import optimizers -import keras.backend as k - -# Preprocessing -from sklearn.preprocessing import MinMaxScaler, StandardScaler - - -def orth_dist(y_true, y_pred): - ''' - Loss function for the NLPCA NN. Returns the sum of the orthogonal - distance from the output tensor to the real tensor. - ''' - loss = tf.math.reduce_sum((y_true - y_pred)**2) - return loss - - -class NLPCA(object): - '''This is a global solver for principal curves that uses neural networks. - - Attributes: - None - ''' - def __init__(self): - self.fit_points = None - self.model = None - self.intermediate_layer_model = None - - def fit(self, data, epochs = 500, nodes = 25, lr = .01, verbose = 0): - '''This method creates a model and will fit it to the given m x n - dimensional data. - - Args: - data (np array): A numpy array of shape (m,n), where m is the - number of points and n is the number of dimensions. - epochs (int): Number of epochs to train neural network, defaults - to 500. - nodes (int): Number of nodes for the construction layers. Defaults - to 25. The more complex the curve, the higher this number - should be. - lr (float): Learning rate for backprop. Defaults to .01 - verbose (0 or 1): Verbose = 0 mutes the training text from Keras. - Defaults to 0. - - Returns: - None - ''' - num_dim = data.shape[1] # get number of dimensions for pts - - # create models, base and intermediate - model = self.create_model(num_dim, nodes = nodes, lr = lr) - bname = model.layers[2].name # bottle-neck layer name - - # The itermediate model gets the output of the bottleneck layer, - # which acts as the projection layer. - self.intermediate_layer_model = Model(inputs=model.input, - outputs=model.get_layer(bname).output) - - # Fit the model and set the instances self.model to model - model.fit(data, data, epochs = epochs, verbose = verbose) - self.model = model - - return - - def project(self, data): - '''The project function will project the points to the curve generated - by the fit function. Given back is the projection index of the original - data and a sorted version of the original data. - - Args: - data (np array): m x n array to project to the curve - - Returns: - proj (array): A one-dimension array that contains the projection - index for each point in data. - all_sorted (array): A m x n+1 array that contains data sorted by - its projection index, along with the index. - ''' - pts = self.model.predict(data) - proj = self.intermediate_layer_model.predict(data) - - self.fit_points = pts - - all = np.concatenate([pts, proj], axis = 1) - all_sorted = all[all[:,2].argsort()] - - return proj, all_sorted - - def create_model(self, num_dim, nodes, lr): - '''Creates a tf model. - - Args: - num_dim (int): How many dimensions the input space is - nodes (int): How many nodes for the construction layers - lr (float): Learning rate of backpropigation - - Returns: - model (object): Keras Model - ''' - # Create layers: - # Function G - input = Input(shape = (num_dim,)) #input layer - mapping = Dense(nodes, activation = 'sigmoid')(input) #mapping layer - bottle = Dense(1, activation = 'sigmoid')(mapping) #bottle-neck layer - - # Function H - demapping = Dense(nodes, activation = 'sigmoid')(bottle) #mapping layer - output = Dense(num_dim)(demapping) #output layer - - # Connect and compile model: - model = Model(inputs = input, outputs = output) - gradient_descent = optimizers.adam(learning_rate=lr) - model.compile(loss = orth_dist, optimizer = gradient_descent) - - return model - - def preprocess(self, data): - '''Converts individual arrays into a singular m x n array, where - m is the number of observations and n is the number of dimensions. - Normalizes the data for faster training. - - Args: - data (list): List of arrays of points. For example, if you have - data for x, y, and z stored in arrays x_, y_, and z_, pass - in [x_, y_, z_] - - Returns: - data_comb (array): A single m x n, where each column - is MinMaxScaled and normed. - ''' - data_lists = [] - - scale = MinMaxScaler(feature_range=(-1,1)) - norm = StandardScaler() - for arr in data: - normed = norm.fit_transform(arr.reshape(-1,1)) - scaled = scale.fit_transform(normed.reshape(-1,1)) - data_lists.append(scaled) - - return np.concatenate(data_lists, axis = 1) \ No newline at end of file diff --git a/prinpy/global_curves/__init__.py b/prinpy/global_curves/__init__.py new file mode 100644 index 0000000..080f49c --- /dev/null +++ b/prinpy/global_curves/__init__.py @@ -0,0 +1,3 @@ +from ._network import _NetworkCurve, NetworkFitter, TrainingCallback + +__all__ = ["_NetworkCurve", "NetworkFitter", "TrainingCallback"] diff --git a/prinpy/global_curves/_network.py b/prinpy/global_curves/_network.py new file mode 100644 index 0000000..af5fea0 --- /dev/null +++ b/prinpy/global_curves/_network.py @@ -0,0 +1,202 @@ +from dataclasses import dataclass +from textwrap import dedent + +from prinpy.interfaces import PrincipalCurve, CurveFitter, Projection +import numpy as np +from prinpy._utils import _check_shape + +from typing import TYPE_CHECKING, Tuple + +try: + import torch + from torch import nn + + _TORCH_AVAILABLE = True +except ImportError: + _TORCH_AVAILABLE = False + +if TYPE_CHECKING: + import torch + from torch import nn + + +if _TORCH_AVAILABLE: + class _OrthogonalLoss(nn.Module): + def __init__(self): + super(_OrthogonalLoss, self).__init__() + + def forward(self, inputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: + # Compute the orthogonal loss as the mean squared distance from inputs to targets + return torch.mean(torch.norm(inputs - targets, dim=1)) + + + class _NetworkCurveModel(nn.Module): + def __init__(self, input_dim: int, hidden_dim: int): + super(_NetworkCurveModel, self).__init__() + self.encoder = nn.Sequential( + nn.Linear(input_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, 1), + ) + self.decoder = nn.Sequential( + nn.Linear(1, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, input_dim), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Returns projection of x onto the curve.""" + return self.decoder(self.encoder(x)) + + def encode(self, x: torch.Tensor) -> torch.Tensor: + """Returns the projection layer output, which is the arc length.""" + return self.encoder(x) + + def decode(self, arc_length: torch.Tensor) -> torch.Tensor: + """Returns the point on the curve corresponding to the given arc length.""" + return self.decoder(arc_length) + + +class _NetworkCurve(PrincipalCurve): + """This class is a curve that is generated by a neural network. The project + function will project the points to the curve generated by the fit function. + + Args: + model (_NetworkCurveModel): The neural network model that generates the curve. + min_arc (float): The minimum arc length of the curve, which corresponds to the + projection of the point on the curve that is closest to the data. + max_arc (float): The maximum arc length of the curve, which corresponds to the + projection of the point on the curve that is farthest from the data. + """ + + def __init__(self, model: "_NetworkCurveModel", min_arc: float, max_arc: float): + self._model = model + self._min_arc = min_arc + self._max_arc = max_arc + + @_check_shape(expected_dim=2) + def project(self, X: np.ndarray) -> Projection: + tensor_X = torch.tensor(X, dtype=torch.float32) + pts = self._model.forward(tensor_X).detach().numpy() + projection = self._model.encode(tensor_X).detach().numpy() - self._min_arc + unit = projection / self.length() + return Projection(points=pts, arc_lengths=projection, unit_lengths=unit) + + @_check_shape(expected_dim=1) + def interpolate_from_length(self, X: np.ndarray) -> Projection: + tensor_X = torch.tensor(X, dtype=torch.float32).reshape(-1, 1) + pts = self._model.decode(tensor_X).detach().numpy() + unit = X / self.length() + return Projection(points=pts, arc_lengths=X, unit_lengths=unit) + + @_check_shape(expected_dim=1) + def interpolate_from_unit(self, X: np.ndarray) -> Projection: + arc_lengths = X * self.length() + self._min_arc + tensor_X = torch.tensor(arc_lengths, dtype=torch.float32).reshape(-1, 1) + pts = self._model.decode(tensor_X).detach().numpy() + return Projection(points=pts, arc_lengths=arc_lengths, unit_lengths=X) + + def length(self) -> float: + return self._max_arc - self._min_arc + + def control_points(self) -> np.ndarray: + """Returns the control points of the curve, which are the decoded points at the min and max arc lengths.""" + with torch.no_grad(): + min_point = self._model.decode(torch.tensor([[self._min_arc]], dtype=torch.float32)).numpy() + max_point = self._model.decode(torch.tensor([[self._max_arc]], dtype=torch.float32)).numpy() + return np.vstack([min_point, max_point]) + + +@dataclass(frozen=True) +class _TrainingRound: + """A dataclass representing a training round for the NetworkFitter. + + Attributes: + epoch (int): The current epoch number. + loss (float): The loss value for the current epoch. + """ + + epoch: int + loss: float + + def __str__(self): + return f"Epoch: {self.epoch}, Loss: {self.loss:.4f}" + + +@dataclass(frozen=True) +class TrainingCallback: + print_progress: bool = True + every_n_epochs: int = 10 + + def __call__(self, training_round: _TrainingRound): + if self.print_progress and training_round.epoch % self.every_n_epochs == 0: + print(training_round) + + +def _throw_need_torch(): + """Raises import error""" + _err = """ + The NetworkFitter requires PyTorch to be installed. + Install via pip: pip install torch + Or, reinstall prinpy with the 'neural' feature: pip install prinpy[neural] + """ + raise ImportError(dedent(_err).strip()) + + +class NetworkFitter(CurveFitter): + """This class is a curve fitter that uses a neural network to fit a curve to + the data. The fit function will create a model and fit it to the data, and + the project function will project the data to the curve generated by the + fit function. The projection index is given by the output of the + intermediate layer of the model, which acts as the projection layer. + """ + + def __init__( + self, + dim: int, + n_hidden: int, + lr: float, + epochs: int, + callback: TrainingCallback | None = None, + ): + if not _TORCH_AVAILABLE: + _throw_need_torch() + + self._model = _NetworkCurveModel(input_dim=dim, hidden_dim=n_hidden) + self._lr = lr + self._epochs = epochs + self._callback = callback + + @_check_shape(expected_dim=2) + def fit(self, data: np.ndarray) -> _NetworkCurve: + data_t = torch.tensor(data, dtype=torch.float32) + + optimizer = torch.optim.Adam(self._model.parameters(), lr=self._lr) + criterion = _OrthogonalLoss() + + for epoch in range(self._epochs): + self._model.train() + optimizer.zero_grad() + outputs = self._model(data_t) + loss = criterion.forward(outputs, data_t) + loss.backward() + optimizer.step() + + # Optionally, print the training progress + if self._callback is not None: + self._callback(_TrainingRound(epoch=epoch, loss=loss.item())) + + self._model.eval() + min_arc, max_arc = self._find_arc_length(self._model, data) + return _NetworkCurve(self._model, min_arc, max_arc) + + def _find_arc_length( + self, model: "_NetworkCurveModel", data: np.ndarray + ) -> Tuple[float, float]: + """Find arc length. Run the data through and get the min and max of the projection layer.""" + with torch.no_grad(): + data_t = torch.tensor(data, dtype=torch.float32) + projection_layer_output = model.encode(data_t).squeeze() + min_arc = projection_layer_output.min().item() + max_arc = projection_layer_output.max().item() + return min_arc, max_arc diff --git a/prinpy/interfaces.py b/prinpy/interfaces.py new file mode 100644 index 0000000..d0183b5 --- /dev/null +++ b/prinpy/interfaces.py @@ -0,0 +1,146 @@ +"""Public interfaces for principal curve fitting. + +This module defines the two core abstractions used throughout prinpy: + +- :class:`PrincipalCurve` — a fitted curve that can project data, interpolate + points, and report its arc length. +- :class:`CurveFitter` — an algorithm that produces a :class:`PrincipalCurve` + from raw data. + +All concrete implementations (spline-based, segment-based, neural-network-based, +etc.) implement these interfaces, so downstream code only ever depends on the +types defined here. +""" + +import numpy as np +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Projection: + """The result of projecting a set of points onto a :class:`PrincipalCurve`. + + All three arrays are parallel: index *i* in each array corresponds to the + same input point. + + Attributes: + arc_lengths: Distances along the curve from its start to each projected + point, measured in the same units as the input data. + Shape ``(n,)``. + unit_lengths: Normalised position of each projected point along the + curve in the range ``[0, 1]``, where ``0`` is the start and ``1`` + is the end. Equivalent to ``arc_lengths / curve.length()``. + Shape ``(n,)``. + points: Coordinates of the projected points on the curve. + Shape ``(n, d)`` where *d* is the dimensionality of the data. + """ + + arc_lengths: np.ndarray + unit_lengths: np.ndarray + points: np.ndarray + + +class PrincipalCurve(ABC): + """Abstract base class for a fitted principal curve. + + A principal curve is a smooth, one-dimensional manifold that passes through + the middle of a dataset. Once fitted, it supports two primary operations: + + - **Projection**: mapping arbitrary data points onto their nearest location + on the curve (see :meth:`project`). + - **Interpolation**: evaluating the curve at a given arc length or + normalised position (see :meth:`interpolate_from_length` and + :meth:`interpolate_from_unit`). + + Concrete subclasses must implement all abstract methods. Consumers of this + interface should type-hint against :class:`PrincipalCurve` rather than any + specific implementation. + """ + + @abstractmethod + def project(self, X: np.ndarray) -> Projection: + """Project data points onto the nearest location on the curve. + + For each point in *X*, finds the closest point on the curve and returns + its coordinates together with the corresponding arc length and + normalised position. + + Args: + X: Input data of shape ``(n, d)``. + + Returns: + A :class:`Projection` containing the projected coordinates, + arc lengths, and unit lengths for every input point. + """ + + @abstractmethod + def interpolate_from_length(self, X: np.ndarray) -> Projection: + """Evaluate the curve at given arc-length positions. + + Args: + X: 1-D array of arc lengths in the range ``[0, curve.length()]``, + shape ``(n,)``. + + Returns: + A :class:`Projection` whose ``points`` are the curve coordinates + at each requested arc length. + """ + + @abstractmethod + def interpolate_from_unit(self, X: np.ndarray) -> Projection: + """Evaluate the curve at given normalised positions. + + Args: + X: 1-D array of unit positions in the range ``[0, 1]``, + shape ``(n,)``. + + Returns: + A :class:`Projection` whose ``points`` are the curve coordinates + at each requested unit position. + """ + + @abstractmethod + def length(self) -> float: + """Total arc length of the curve. + + Returns: + The arc length measured in the same units as the input data. + """ + + @abstractmethod + def control_points(self) -> np.ndarray: + """Key points that define or summarise the shape of the curve. + + The meaning of "control points" varies by implementation — they may be + spline knots, segment endpoints, or representative samples — but they + always lie on the curve and are ordered from start to end. + + Returns: + Array of shape ``(k, d)`` where *k* is the number of control points + and *d* is the dimensionality of the data. + """ + + +class CurveFitter(ABC): + """Abstract base class for algorithms that fit a :class:`PrincipalCurve`. + + Subclasses encapsulate a specific fitting strategy (e.g. greedy segment + growing, SVD-based fitting, or neural-network optimisation). The fitted + curve is always returned as a :class:`PrincipalCurve` so that callers + remain decoupled from the underlying implementation. + """ + + @abstractmethod + def fit(self, data: np.ndarray) -> PrincipalCurve: + """Fit a principal curve to the given data. + + Args: + data: Input data of shape ``(n, d)`` where *n* is the number of + samples and *d* is the number of dimensions. + + Returns: + A fitted :class:`PrincipalCurve` that summarises the structure of + *data*. + """ + diff --git a/prinpy/local.py b/prinpy/local.py deleted file mode 100644 index c214f58..0000000 --- a/prinpy/local.py +++ /dev/null @@ -1,359 +0,0 @@ -''' -These are local algorithms. These work on a per-step basis. Starting -at one point, the algorithm attempts to choose the next best point, -and so on until the end is reached. -''' - -# Import some modules -import numpy as np -import matplotlib.pyplot as plt -import scipy.optimize as op -import scipy.interpolate - - -def distg(pts, v1, v2): - ''' - Returns the minimum of points from line and points from vertex - pts: points to calulate distance from - v1: vertex j - v2: vertex j+1 - ''' - D1 = np.linalg.norm(v2 - pts, axis = 1) - D2 = np.abs(np.cross(v2-v1, v1-pts)) / np.linalg.norm(v2-v1) - - error_t = [np.min([i,j]) for i,j in zip(D1, D2)] - - return sum(error_t)/len(error_t) - -def points_in(pts, r1, p): - ''' - Gets points in r1 from p - ''' - distances = np.linalg.norm(p - pts, axis = 1) - return pts[(distances < (r1))] - -def points_out(pts, r1, p): - ''' - Gets points out r1 from p - ''' - distances = np.linalg.norm(p - pts, axis = 1) - return pts[(distances > r1)] - -def points_btw(pts, r1, r2, p): - ''' - Gets points between r1 < r2 from p - ''' - distances = np.linalg.norm(p - pts, axis = 1) - return pts[(distances < r2) & (distances > r1)] - -def reset(x,y): - dat = np.concatenate([x.reshape(-1,1), y.reshape(-1,1)], axis = 1) - return dat, [dat[0,:]] - -def proj_min(X, tck, pt): - ''' - Finds the distance X along the PC where pt has the shortest - projection distance. - ''' - loc_ = scipy.interpolate.splev(X, tck) - return np.linalg.norm(loc_ - pt) - -class CLPCG: - def __init__(self): - self.fit_points = [] - self.spline_ticks = None - - def points(self, x, y, e_max = .2, fmin_error = False): - '''Implements CLPC-greedy algorithm. - - Args: - x (array): x-data to fit - y (array): y-data to fit - e_max (flat): Max allowed error. If not met, another point P will - be addedto the curve. Authors suggest 1/4 to 1/2 of - measurement error. Defaults to .2 - - Returns: - points (array): collection of points that construct the straight - line segments. - ''' - # Combine x,y and sort - data = np.concatenate([x.reshape(-1,1), y.reshape(-1,1)], axis = 1) - - points = [] # points of principal curve - points.append(data[0,:]) # Append first point - pe = data[-1,:] # end point - - while 1: - pt_found = False - - # Start drawing circle - rl = 0 # lower radius bound - rt = 2 * np.linalg.norm(pe - points[-1]) # upper bound - - # First, attempt to connect to end point. - # Connecting to the end point with acceptable ei is the - # termination condition. - rend = np.linalg.norm(pe - points[-1]) - in_c = points_in(data, rend, points[-1]) #get pts inside circle - try: - e_end = distg(in_c, points[-1], pe) # calculate error to end pt - except ZeroDivisionError: # successfully terminates, weird case - break - - if e_end <= e_max: - points.append(pe) - break - - while not pt_found: # point with acceptable error not found - # begin draw circle with radius ri - ri = rl + (rt - rl)/2 - - in_c = points_in(data, ri, points[-1]) #get pts inside circle - rj = ri * .9 # Construct inner radius - btw_c = points_btw(data, rj, ri, points[-1]) #get pts btw circle - - if btw_c.shape[0] == 0: # No points s.t. rj > ||p|| > ri - raise ValueError("e_max = %f is too small. Choose a " \ - "larger e_max." % e_max) - else: - # candidate point is mean of points in circle sector - p2 = np.array([np.mean(btw_c[:,0]), np.mean(btw_c[:,1])]) - e_i = distg(in_c, points[-1], p2) # calculate error - - if e_i > e_max: # if error not acceptable, reduce size of rt - rt = ri - - # If error is acceptable, add p2 to points - else: - data = points_out(data, ri, points[-1]) - points.append(p2) - pt_found = True - - # transform points into an array - res_x = np.array([p[0] for p in points]) - res_y = np.array([p[1] for p in points]) - res = np.concatenate([res_x.reshape(-1,1), res_y.reshape(-1,1)], axis = 1) - - if res.shape[0] <= 3: - raise ValueError("Not enough points generated: Spline degre 3 with" \ - " %d points generated. Try reducing e_max" \ - % (res.shape[0])) - - self.fit_points = res - return res - - def fit(self, x, y, e_max = .2, fmin_error = False): - ''' - Calculates principal curve ticks - - Args: same as points - - Returns: - None - ''' - res = self.points(x, y, e_max, fmin_error) - tck, u = scipy.interpolate.splprep(res.T, s = 0) - self.spline_ticks = tck - - return - - def plot(self, ax = None): - ''' - Plots the curve to a MPL axes object. - - Args: - ax (object): Optional set of ax to plot to. If None, a set of ax - will be created. - ''' - if ax == None: - fig, ax = plt.subplots() - xy = scipy.interpolate.splev(np.linspace(0,1,100), self.spline_ticks) - ax.plot(xy[0], xy[1], c = 'black') - return - - def project(self, x, y): - ''' - Projects points x,y to principal curve calculated by calc_pc - Args: - x (array): x-data to project - y (array): y-data to project - Returns: - proj (array): Projecton index of points onto curve between (0,1) - ''' - # for each point min distance to curve - data = np.concatenate([x.reshape(-1,1), y.reshape(-1,1)], axis = 1) - - proj = [] - for p in data: - proj_dist = op.minimize( - proj_min, - x0 = [.5], - args = (self.spline_ticks, p), - method = 'Powell' - ).x - proj.append(proj_dist) - return proj - -# Functions specific to the search alg, namely finding the -# error line and the function we search with/minimize -def to_min_error(theta, pts, v1, r1): - v2 = np.array([v1[0]+r1*np.cos(theta[0]), v1[1]+r1*np.sin(theta[0])]) - D1 = np.linalg.norm(v2 - pts, axis = 1) - D2 = np.abs(np.cross(v2-v1, v1-pts)) / np.linalg.norm(v2-v1) - - error_t = [np.min([i,j]) for i,j in zip(D1, D2)] - - return sum(error_t)/len(error_t) - -def error_line(theta, c, r1): - p2 = np.array([c[0]+r1*np.cos(theta), c[1]+r1*np.sin(theta)]) - return p2 - -def point_dist(pts, v1, v2): - ''' - Tells CLPCS if it should invert direction of best fit line - v1: vertex j - v2: vertex j+1 - ''' - D1 = np.linalg.norm(v2 - pts, axis = 1) - return sum(D1)/len(D1) - -class CLPCS: - def __init__(self): - self.fit_points = [] - self.spline_ticks = None - - def points(self, x, y, e_max = .2, rl = 0): - '''Implements CLPC one dimensional search algorithm - - Args: - x (array): x-data to fit - y (array): y-data to fit - e_max (flat): Max allowed error. If not met, another point P will - be addedto the curve. Authors suggest 1/4 to 1/2 of - measurement error. Defaults to .2 - - Returns: - points (array): collection of points that construct the straight - line segments. - ''' - # Combine x,y and sort - data = np.concatenate([x.reshape(-1,1), y.reshape(-1,1)], axis = 1) - - points = [] # points of principal curve - points.append(data[0,:]) # Append first point - pe = data[-1,:] # end point - - while 1: - pt_found = False - - # Start drawing circle - rt = 2 * np.linalg.norm(pe - points[-1]) # upper bound - - # First, attempt to connect to end point. - # Connecting to the end point with acceptable ei is the - # termination condition. - rend = np.linalg.norm(pe - points[-1]) - in_c = points_in(data, rend, points[-1]) #get pts inside circle - try: - e_end = distg(in_c, points[-1], pe) # calculate error to end pt - except ZeroDivisionError: # successfully terminates, weird case - break - - if e_end <= e_max: - points.append(pe) - break - - while not pt_found: - ri = rl + (rt - rl)/2 - - # Get points inside circle - in_c = points_in(data, ri, points[-1]) - - if in_c.shape[0] == 0: # No points are in circle - raise ValueError("e_max = %f is too small. Choose a " \ - "smaller e_max." % e_max) - - else: - # find min error - theta = op.minimize( - to_min_error, - x0 = [0], - args = (in_c, points[-1], ri), - method = 'Powell' - ).x - p2 = error_line(theta, points[-1], ri) - e_i = distg(in_c, points[-1], p2) # calculate error - p_error = point_dist(in_c, points[-1], p2) - - # Try to invert p2 and check error. This is because - # the optimzation alg can fail to account for the fact - # that p2 could be closer to points than drawing other dir - theta_inv = np.pi + theta - p2_inv = error_line(theta_inv, points[-1], ri) - p_error_inv = point_dist(in_c, points[-1], p2_inv) - - if p_error_inv < p_error: p2 = p2_inv - - if e_i >= e_max: - rt = ri - else: - data = points_out(data, ri, points[-1]) - points.append(p2) - - pt_found = True - - res_x = np.array([p[0] for p in points]) - res_y = np.array([p[1] for p in points]) - res = np.concatenate([res_x.reshape(-1,1), res_y.reshape(-1,1)], axis = 1) - return res - - def fit(self, x, y, e_max = .2, rl = 0): - ''' - Calculates principal curve ticks - Args: same as fit_points - - Returns: - None - ''' - res = self.points(x, y, e_max, rl) - tck, u = scipy.interpolate.splprep(res.T, s = 0) - self.spline_ticks = tck - return - - def plot(self, ax = None, **kwargs): - ''' - Plots the curve to a MPL axes object. - Args: - ax (object): Optional set of ax to plot to. If None, a - set of ax will be created. - ''' - if ax == None: - fig, ax = plt.subplots() - xy = scipy.interpolate.splev(np.linspace(0,1,100), self.spline_ticks) - ax.plot(xy[0], xy[1], c = 'black', **kwargs) - return - - def project(self, x, y): - ''' - Projects points x,y to principal curve calculated by calc_pc - Args: - x (array): x-data to project - y (array): y-data to project - Returns: - proj (array): projections of points onto curve between (0,1) - ''' - # for each point min distance to curve - data = np.concatenate([x.reshape(-1,1), y.reshape(-1,1)], axis = 1) - - proj = [] - for p in data: - proj_dist = op.minimize( - proj_min, - x0 = [.5], - args = (self.spline_ticks, p), - method = 'Powell' - ).x - proj.append(proj_dist) - return proj diff --git a/prinpy/local_curves/__init__.py b/prinpy/local_curves/__init__.py new file mode 100644 index 0000000..a94e09b --- /dev/null +++ b/prinpy/local_curves/__init__.py @@ -0,0 +1,7 @@ +from ._constrained import ConstrainedFitter, GreedyFit, SVDFit + +__all__ = [ + "ConstrainedFitter", + "GreedyFit", + "SVDFit", +] diff --git a/prinpy/local_curves/_constrained.py b/prinpy/local_curves/_constrained.py new file mode 100644 index 0000000..e02aa74 --- /dev/null +++ b/prinpy/local_curves/_constrained.py @@ -0,0 +1,102 @@ +""" +This file implements algorithms from +https://www.sciencedirect.com/science/article/pii/S0377042715005956 +""" + +from prinpy.interfaces import CurveFitter, PrincipalCurve, Projection +from prinpy._rs import clpg, clppca +from prinpy._containers import _SplineCurve, _SegmentCurve +from prinpy._utils import _check_shape +import numpy as np +from dataclasses import dataclass + + +class FitAlgorithm: + pass + + +@dataclass(frozen=True) +class GreedyFit(FitAlgorithm): + """ + GreedyFit class for fitting curves using a greedy algorithm. + + Implementation of CLPCg (CLPC using greedy thinking) algorithm (section 3.2 of paper). + Sets the next vertex to the mean of the points within the current radius but beyond an inner radius. + If the current radius is r, then the next vertex is the mean of points that satisfy r * inner_radius < distance <= r. + + Args: + inner_radius (float): The inner radius parameter for the greedy fit algorithm. + """ + + inner_radius: float = 0.9 + + +@dataclass(frozen=True) +class SearchFit(FitAlgorithm): + """ + SearchFit class for fitting curves using a search algorithm. + + Implementation of CLPCs (CLPC using one dimensional search) algorithm (section 3.3 of paper). + Sets the next vertex to the point that minimizes the local fitting error among a set of candidates. + + Args: + trials (int): The number of candidate points to evaluate for the next vertex. + """ + + trials: int = 20 + + +@dataclass(frozen=True) +class SVDFit(FitAlgorithm): + """ + SVDFit class for fitting + curves using Singular Value Decomposition (SVD). + """ + + pass + + +class ConstrainedFitter(CurveFitter): + """ + ConstrainedFitter class for fitting curves with constraints. + + Fits a piecewise-linear curve to ordered data using the + Constrained Local Principal Curve (CLPC) procedure. + + Starting from an initial vertex, the algorithm repeatedly: + 1. Gathers all points within an adaptive radius of the current + vertex, preserving their original ordering. + 2. Computes the next vertex using a FitAlgorithm (e.g., GreedyFit or SearchFit). + 3. Projects the points onto the candidate segment and evaluates + the local fitting error. + 4. Accepts the segment if the error is below a threshold, then + discards the points used for this segment. + 5. Expands or shrinks the radius when the error is too large, + retrying until a valid segment is found. + + The process continues until no points remain, producing a monotone, + forward-marching polyline that respects local geometric constraints. + + Args: + algorithm (FitAlgorithm): The algorithm to use for fitting the curve. + tolerance (float): The tolerance for the fitting algorithm. + """ + + def __init__(self, algorithm: FitAlgorithm, tolerance: float = 1e-3): + self._algorithm = algorithm + self._tolerance = tolerance + + @_check_shape(2) + def fit(self, data: np.ndarray) -> PrincipalCurve: + data = np.ascontiguousarray(data, dtype=np.float32) + match self._algorithm: + case GreedyFit(inner_radius=inner_radius): + fit_points = clpg(data, self._tolerance, inner_radius) + case SVDFit(): + fit_points = clppca(data, self._tolerance) + case _: + raise ValueError(f"Unsupported algorithm: {self._algorithm}") + + if fit_points.shape[0] < 4: + return _SegmentCurve(fit_points) + return _SplineCurve(fit_points) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2cf68ed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["maturin>=1.5,<2.0"] +build-backend = "maturin" + +[project] +name = "prinpy" +version = "1.0.0" +license = "MIT" +description = "A high-performance package for fitting principal curves in Python" +authors = [{name = "Matthew Artuso", email = "artusoma1@gmail.com"}] +readme = "README.md" +requires-python = ">=3.9" +keywords = ["principal curves", "dimensionality reduction", "machine learning", "statistics"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering", +] +dependencies = [ + "numpy>=1.20", + "scipy>=1.7", +] + +[project.optional-dependencies] +neural = ["torch>=2.0"] +dev = [ + "pytest>=7.0", + "matplotlib>=3.5", + "seaborn>=0.12", +] + +[project.urls] +Homepage = "https://github.com/artusoma/prinpy" +Repository = "https://github.com/artusoma/prinpy" +"Bug Tracker" = "https://github.com/artusoma/prinpy/issues" + +[tool.pytest.ini_options] +addopts = "--ignore=tests/global_curves" + +[tool.maturin] +module-name = "prinpy._rs.prinpy_rs" +python-source = "." diff --git a/setup.py b/setup.py deleted file mode 100644 index 4d5e78b..0000000 --- a/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup - -with open("README.md", 'r') as f: - long_description = f.read() - -setup( - name = 'prinpy', - version = '0.0.3.1', - license = "MIT", - description = "A package for fitting principal curves in Python", - author = "https://github.com/artusoma/", - author_email = 'artusoma1@gmail.com', - url = 'https://github.com/artusoma/prinPy', - packages = ["prinpy"], - long_description = long_description, - long_description_content_type='text/markdown', - install_requires = ['numpy', - 'matplotlib', - 'scipy', - 'keras', - 'tensorflow', - ], -) \ No newline at end of file diff --git a/src/global.rs b/src/global.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b17eb73 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,76 @@ +pub mod global; +pub mod local; +pub mod utilities; + +use pyo3::prelude::*; + +macro_rules! to_pyerr { + ($err:ident) => { + PyRuntimeError::new_err(format!("Rust Runtime Error: {}", $err)) + }; +} + +#[pymodule] +mod prinpy_rs { + use super::local::{ConstrainedFitIterator, GreedyFitter, SVDFitter}; + use super::utilities::find_nearest_candidates; + use pyo3::prelude::*; + + use ndarray::Array1; + use numpy::{PyArray2, PyReadonlyArray2}; + use pyo3::exceptions::PyRuntimeError; + + #[pyfunction] + fn clpg<'py>( + py: Python<'py>, + x: PyReadonlyArray2<'py, f32>, + tol: f32, + inner_radius: f32, + ) -> PyResult>> { + let array_view = x.as_array(); + let iterator = + ConstrainedFitIterator::new(&array_view, tol, GreedyFitter::new(inner_radius)); + + let fit_points: Vec> = iterator + .collect::, _>>() + .map_err(|e| to_pyerr!(e))?; + + let view_points: Vec<_> = fit_points.iter().map(|v| v.view()).collect(); + + let result_array = + ndarray::stack(ndarray::Axis(0), &view_points).map_err(|e| to_pyerr!(e))?; + + Ok(PyArray2::from_array(py, &result_array)) + } + + #[pyfunction] + fn clppca<'py>( + py: Python<'py>, + x: PyReadonlyArray2<'py, f32>, + tol: f32, + ) -> PyResult>> { + let array_view = x.as_array(); + let iterator = ConstrainedFitIterator::new(&array_view, tol, SVDFitter::default()); + + let fit_points: Vec> = iterator + .collect::, _>>() + .map_err(|e| to_pyerr!(e))?; + + let view_points: Vec<_> = fit_points.iter().map(|v| v.view()).collect(); + + let result_array = + ndarray::stack(ndarray::Axis(0), &view_points).map_err(|e| to_pyerr!(e))?; + + Ok(PyArray2::from_array(py, &result_array)) + } + + #[pyfunction] + fn find_nearest_points<'py>( + data: PyReadonlyArray2<'py, f32>, + curve: PyReadonlyArray2<'py, f32>, + ) -> Vec { + let data = data.as_array(); + let curve = curve.as_array(); + find_nearest_candidates(&data, &curve) + } +} diff --git a/src/local.rs b/src/local.rs new file mode 100644 index 0000000..c025b15 --- /dev/null +++ b/src/local.rs @@ -0,0 +1,5 @@ +pub mod constrained; +pub mod fitters; + +pub use constrained::ConstrainedFitIterator; +pub use fitters::{GreedyFitter, SVDFitter}; \ No newline at end of file diff --git a/src/local/constrained.rs b/src/local/constrained.rs new file mode 100644 index 0000000..d472412 --- /dev/null +++ b/src/local/constrained.rs @@ -0,0 +1,197 @@ +//! Module implements constained local principal curve algorithms +//! https://www.sciencedirect.com/science/article/pii/S0377042715005956?via%3Dihub#s000090 + +use crate::utilities::*; +use ndarray::{Array1, Array2, ArrayRef1, ArrayRef2, ArrayViewMut1, ArrayViewMut2, s}; +use thiserror::Error; + +/// Trait that performs step 4 in 3.1 +pub trait Fitter { + /// Accepts points in circle, their distances, the max circle, and returns best-fit point + fn fit_segment( + &self, + data: &ArrayRef2, + sq_distances: &ArrayRef1, + sq_radius: f32, + vertex: &ArrayRef1, + ) -> Result, ConstrainedFitError>; +} + +/// Error type +#[derive(Error, Debug)] +pub enum ConstrainedFitError { + #[error("No points were found in computational area.")] + EmptySliceError, + #[error("No points in found in radius. Please increase error tolerance.")] + NoPointsInRadius, +} + +/// Iterator that yields the vertices of a constrained local principal curve fit. +/// +/// Implements the algorithm from Kégl et al. (2015), section 3.1. Starting from the first +/// data point, each call to [`next`] finds the next best-fit vertex by: +/// 1. Collecting all points within a search radius of the current vertex. +/// 2. Delegating to a [`Fitter`] to find the candidate next vertex. +/// 3. Checking that the mean perpendicular error of points in the radius is within `max_error`. +/// 4. Halving the radius and retrying if the error check fails. +/// +/// The first item yielded is always the first data point. The last item yielded is always +/// the last data point. Points are consumed and discarded as they are covered by a segment. +#[derive(Debug)] +pub struct ConstrainedFitIterator { + /// Data to fit + data: Array2, + /// Algorithm to compute next point + fitter: F, + /// Max tolerable error before shrinking local search + max_error: f32, + /// Remaining points in data that are not fit yet. + remaining: usize, + /// Previous vertex to build from + current_vertex: Array1, + /// Final vertex to connect to + final_vertex: Array1, + /// If we just started; used to return the initial vertex + initializing: bool, +} + +impl std::iter::Iterator for ConstrainedFitIterator { + type Item = Result, ConstrainedFitError>; + + fn next(&mut self) -> Option { + if self.initializing { + self.initializing = false; + return Some(Ok(self.current_vertex.clone())); + } + + if self.remaining == 0 { + return None; + } + + Some(self.get_next_vertex()) + } +} + +/// Partitions `data` and `distances` in-place so that points outside `radius` occupy +/// `0..split` and points inside occupy `split..len`, returning the split index. +/// +/// Both slices are reordered identically, so `distances[i]` always corresponds to `data[i]`. +/// The relative order within each partition is not preserved. +fn partition_on_distance( + radius: f32, + data: &mut ArrayViewMut2, + distances: &mut ArrayViewMut1, +) -> usize { + // Walk data. Partition data and distances into two sections: + // 0..in = inside radius + // in..end = inside radius (will drop on success) + let mut write_idx: usize = data.nrows() - 1; + for read_idx in (0..data.nrows()).rev() { + // If read is in the radius, swap with write + if distances[read_idx] <= radius { + // Check to avoid double mutable borrow + if read_idx != write_idx { + swap_row!(data, read_idx, write_idx, ..); + swap_row!(distances, read_idx, write_idx); + } + write_idx -= 1; + } + } + write_idx + 1 +} + +fn calc_error( + data: &ArrayRef2, + v1: &ArrayRef1, + v2: &ArrayRef1, +) -> Result { + distance_line_to_point(data, v1, v2) + .mean() + .ok_or(ConstrainedFitError::EmptySliceError) +} + +impl ConstrainedFitIterator { + /// Advances the iterator by computing the next principal curve vertex. + /// + /// Starting from `current_vertex`, the search radius is initialised to the squared distance + /// to `final_vertex`. The algorithm then loops: + /// + /// 1. Partitions the remaining data into points inside and outside the radius. + /// 2. Calls [`Fitter::fit_segment`] on the inside points to get a candidate vertex. + /// 3. Computes the mean perpendicular error of inside points to the segment + /// `current_vertex → candidate`. + /// 4. If `error <= max_error`, accepts the candidate: discards covered points, + /// updates `current_vertex`, and returns `Ok(candidate)`. + /// 5. Otherwise halves the squared radius and retries. + /// + /// Returns [`ConstrainedFitError::NoPointsInRadius`] if the radius shrinks until + /// no points remain inside it. + fn get_next_vertex(&mut self) -> Result, ConstrainedFitError> { + let mut sq_radius: f32 = squared_distance(&self.current_vertex, &self.final_vertex) / 2.0; + + let mut data_ref = self.data.slice_mut(s![0..self.remaining, ..]); + + // Check if we can complete curve to end + if calc_error(&data_ref, &self.current_vertex, &self.final_vertex)? <= self.max_error { + self.remaining = 0; + self.current_vertex = self.final_vertex.clone(); + return Ok(self.final_vertex.clone()); + } + + // Get distances from vertex to + let mut sq_distances = data_ref + .rows() + .into_iter() + .map(|p| squared_distance(&p, &self.current_vertex)) + .collect::>(); + + loop { + let cnt_out = partition_on_distance( + sq_radius, + &mut data_ref, + &mut sq_distances.slice_mut(s![..]), + ); + + let in_circle = data_ref.slice(s![cnt_out..data_ref.nrows(), ..]); + let in_distances = sq_distances.slice(s![cnt_out..data_ref.nrows()]); + + if in_circle.nrows() == 0 { + return Err(ConstrainedFitError::NoPointsInRadius); + } + + // Use fitter to find new segment + let candidate_point = self.fitter.fit_segment( + &in_circle, + &in_distances, + sq_radius, + &self.current_vertex, + )?; + + // Project found points in radius from segment from Pi to Pi+1, getting local error Ei + let error = calc_error(&in_circle, &self.current_vertex, &candidate_point)?; + + // If Ei <= Emax, discard used points and break with candidate + if error <= self.max_error { + self.remaining = cnt_out; + self.current_vertex = candidate_point.to_owned(); + break Ok(candidate_point); + } + sq_radius /= 2.; + } + } + + pub fn new(data: &ArrayRef2, max_error: f32, fitter: F) -> Self { + let current_vertex = data.row(0).to_owned(); + let final_vertex = data.row(data.nrows() - 1).to_owned(); + let remaining: usize = data.nrows(); + Self { + data: data.to_owned(), + fitter, + max_error, + remaining, + current_vertex, + final_vertex, + initializing: true, + } + } +} diff --git a/src/local/fitters.rs b/src/local/fitters.rs new file mode 100644 index 0000000..8159c42 --- /dev/null +++ b/src/local/fitters.rs @@ -0,0 +1,91 @@ +use super::constrained::{ConstrainedFitError, Fitter}; +use crate::utilities::compute_pc1; +use ndarray::{Array1, ArrayRef1, ArrayRef2, Axis}; + +/// Greedy fitter uses a narrow width around the edge of the circle to compute the +/// mean, using that as the next vertex +#[derive(Debug)] +pub struct GreedyFitter { + slice_width: f32, +} + +impl Default for GreedyFitter { + fn default() -> Self { + GreedyFitter { slice_width: 0.9 } + } +} + +impl GreedyFitter { + pub fn new(slice_width: f32) -> Self { + Self { slice_width } + } +} + +impl Fitter for GreedyFitter { + fn fit_segment( + &self, + data: &ArrayRef2, + dist: &ArrayRef1, + radius: f32, + _vertex: &ArrayRef1, + ) -> Result, ConstrainedFitError> { + let inner_radius = (self.slice_width * radius.sqrt()).powi(2); + let indices: Vec = (0..dist.len()) + .filter(|&idx| dist[idx] > inner_radius) + .collect(); + + if indices.is_empty() { + // FALLBACK: If no points are in the outer shell, just take the mean of all points + // in the circle + return data + .mean_axis(Axis(0)) + .ok_or(ConstrainedFitError::EmptySliceError); + } + + let in_manifold = data.select(Axis(0), &indices); + in_manifold + .mean_axis(Axis(0)) + .ok_or(ConstrainedFitError::EmptySliceError) + } +} + +/// Implementation for a fitter that performs PCA on points inside of the radius +/// to find the direction that minimizes variance +#[derive(Debug)] +pub struct SVDFitter; + +impl Default for SVDFitter { + fn default() -> Self { + Self + } +} + +impl Fitter for SVDFitter { + fn fit_segment( + &self, + data: &ArrayRef2, + _sq_distances: &ArrayRef1, + sq_radius: f32, + vertex: &ArrayRef1, + ) -> Result, ConstrainedFitError> { + // Center data to vertex. + // Unwrap because this should not fail. + let centered = data - vertex.to_shape((1, vertex.len())).unwrap().to_owned(); + + // PC1 is a unit vector. Scale by R and add back centering + let mut direction = compute_pc1(¢ered); + + // Check if the direction vector points with or against the average data trend. + // We can dot product our direction with the sum of all centered points. + let data_trend = centered.sum_axis(Axis(0)); + if direction.dot(&data_trend) < 0.0 { + direction *= -1.0; // Flip it so it points toward the points + } + + // Scale by R and add back the anchor vertex origin + let radius = sq_radius.sqrt(); + let endpoint = (direction * radius) + vertex; + + Ok(endpoint) + } +} diff --git a/src/utilities.rs b/src/utilities.rs new file mode 100644 index 0000000..4d20cb2 --- /dev/null +++ b/src/utilities.rs @@ -0,0 +1,104 @@ +//! General utilities + +use ndarray::Zip; +use ndarray::prelude::*; + +/// swap_row! swaps elements in `array` between `row_a` and `row_b` +#[macro_export] +macro_rules! swap_row { + ($array:ident, $row_a:ident, $row_b:ident $(, $indices:expr),*) => { + let (mut x, mut y) = $array.multi_slice_mut((s![$row_a $(, $indices),*], s![$row_b $(, $indices),*])); + x.iter_mut() + .zip(y.iter_mut()) + .for_each(|(a, b)| std::mem::swap(a, b)); + }; +} +pub(crate) use swap_row; + +pub fn distance_line_to_point( + data: &ArrayRef2, + v1: &ArrayRef1, + v2: &ArrayRef1, +) -> Array1 { + let l = Zip::from(v1).and(v2).map_collect(|v1, v2| v2 - v1); + let l_dot_l = l.dot(&l); + Zip::from(data.rows()).map_collect(|pt| { + let (dot_pp, dot_pl) = pt.iter().zip(v1.iter()).zip(l.iter()).fold( + (0f32, 0f32), + |(dot1, dot2), ((ptv, v1v), lv)| { + let d = ptv - v1v; + (dot1 + d * d, dot2 + d * lv) + }, + ); + (dot_pp - dot_pl * dot_pl / l_dot_l).sqrt() + }) +} + +pub fn outer_product(x: &ArrayRef1, y: &ArrayRef1) -> Array2 { + let (size_x, size_y) = (x.shape()[0], y.shape()[0]); + let x_reshaped = x.view().into_shape_with_order((size_x, 1)).unwrap(); + let y_reshaped = y.view().into_shape_with_order((1, size_y)).unwrap(); + x_reshaped.dot(&y_reshaped) +} + +pub fn squared_distance(v1: &ArrayRef1, v2: &ArrayRef1) -> f32 { + v1.iter() + .zip(v2.iter()) + .fold(0f32, |dot, (&a, &b)| dot + (a - b).powi(2)) +} + +fn get_best_candidate(row: &ArrayRef1, candidates: &ArrayRef2) -> usize { + candidates + .rows() + .into_iter() + .enumerate() + .fold( + (f32::INFINITY, 0usize), + |(dist, idx), (new_idx, candidate_row)| { + let dot = squared_distance(row, &candidate_row); + if dot < dist { + (dot, new_idx) + } else { + (dist, idx) + } + }, + ) + .1 +} + +pub fn find_nearest_candidates(data: &ArrayRef2, candidates: &ArrayRef2) -> Vec { + data.rows() + .into_iter() + .map(|row| get_best_candidate(&row, &candidates)) + .collect::>() +} + +pub fn compute_pc1(data: &Array2) -> Array1 { + let b = data.t().dot(data); + let ncols = data.ncols(); + + let mut v: Array1 = Array1::from_elem(ncols, 1.0); + let norm = v.dot(&v).sqrt(); + v.mapv_inplace(|x| x / norm); // Idiomatic in-place division + + let mut prev_v = v.clone(); + + for _ in 0..50 { + let mut v_new = b.dot(&v); + + let norm = v_new.dot(&v_new).sqrt(); + v_new.mapv_inplace(|x| x / norm); + + let dot_product = v_new.dot(&prev_v).abs(); + if 1.0 - dot_product < 1e-6 { + v = v_new; + break; + } + + // Update vectors for the next iteration + prev_v = v_new.clone(); + v = v_new; + } + + v +} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/global_curves/test_network.py b/tests/global_curves/test_network.py new file mode 100644 index 0000000..b28daf5 --- /dev/null +++ b/tests/global_curves/test_network.py @@ -0,0 +1,10 @@ +import pytest +import numpy as np + +pytest.importorskip("torch") +from prinpy.global_curves import _NetworkCurve, NetworkFitter + +def test_fitter_raise_shape_error(): + fitter = NetworkFitter(dim=2, n_hidden=10, lr=0.01, epochs=10) + with pytest.raises(ValueError): + fitter.fit(np.random.rand(10)) diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..10cf3ad --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,2 @@ +def test_dummy(): + pass