fpm-py


Namefpm-py JSON
Version 2.0.1 PyPI version JSON
download
home_pageNone
SummaryFourier ptychography tooling for simulation and reconstruction
upload_time2025-10-08 04:38:42
maintainerNone
docs_urlNone
authorNone
requires_python<3.13,>=3.9
licenseNone
keywords fourier ptychography microscopy imaging reconstruction
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # fpm-py

A Python library for Fourier Ptychographic Microscopy (FPM) simulation and reconstruction with GPU acceleration.

## Overview

This library implements both the **forward pass** (reconstruction from captures) and **backward pass** (simulation of captures from an object) for Fourier ptychography. It uses PyTorch for efficient GPU-accelerated computation and provides a clean, type-safe API for research and development.

Fourier ptychography synthesizes a high-resolution, wide-field image by computationally combining multiple low-resolution images captured under varying illumination angles. This creates a synthetic aperture larger than the physical objective NA, enabling resolution enhancement while maintaining a large field of view.

## Installation

```bash
pip install fpm-py
```

Or with `uv` (preferred):
```bash
uv add fpm-py
```

**Requirements**: 3.12 ≥ Python ≥ 3.9, PyTorch ≥ 2.8.0

## Quick Start

```python
import fpm_py as fpm
from fpm_py.utils.data_utils import image_to_tensor
import torch

# Load a test object
obj = image_to_tensor("path/to/image.png", to_complex=True)

# Define k-vectors (illumination angles) - 5×5 grid
grid_size = 5
spacing = 0.2
k_range = torch.linspace(-(grid_size // 2), grid_size // 2, grid_size) * spacing
kx, ky = torch.meshgrid(k_range, k_range, indexing="ij")
k_vectors = torch.stack((kx.flatten(), ky.flatten()), dim=1)

# Simulate FPM captures (backward pass)
dataset = fpm.kvectors_to_image_series(
    obj=obj,
    k_vectors=k_vectors,
    pupil_radius=100,
    wavelength=550e-9,      # 550 nm (green light)
    pixel_size=1e-6,        # 1 μm pixels
    magnification=10.0      # 10× objective
)

# Reconstruct high-resolution image (forward pass)
reconstruction = fpm.reconstruct(dataset, output_scale_factor=4, max_iters=10)

# View magnitude
import matplotlib.pyplot as plt
plt.imshow(reconstruction.abs().cpu().numpy(), cmap='gray')
plt.show()
```

For a more detailed walkthrough, see [main.py](https://github.com/rspcunningham/fpm-py/blob/main/main.py).

## Core Concepts

### Data Structures

#### `ImageCapture`
Represents a single FPM capture with its associated k-vector:
```python
@dataclass(frozen=True)
class ImageCapture:
    image: torch.Tensor      # (H, W) intensity image
    k_vector: torch.Tensor   # (2,) illumination angle in k-space
```

#### `ImageSeries`
Container for multiple captures with acquisition settings:
```python
@dataclass
class ImageSeries:
    captures: Sequence[ImageCapture]
    settings: AcquisitionSettings  # du, wavelength, pixel_size
    device: torch.device
```

The `du` parameter is the **k-space lattice spacing** — it relates pixel coordinates in Fourier space to physical spatial frequencies. Automatically computed as:

```python
du = wavelength / (N × effective_pixel_size)
```

where `effective_pixel_size = pixel_size / magnification`.

### Backward Pass: Simulation

Generate synthetic FPM captures from a known object (inverse of reconstruction):

```python
series = fpm.kvectors_to_image_series(
    obj=complex_field,           # Complex object (amplitude + phase)
    k_vectors=k_vectors,         # (N, 2) illumination directions
    pupil_radius=100,            # Pupil radius in pixels (related to NA)
    wavelength=550e-9,           # Illumination wavelength [m]
    pixel_size=1e-6,             # Camera pixel size [m]
    magnification=10.0,          # Optical magnification
    pupil=None                   # Optional custom pupil function
)
```

**Physics**: For each k-vector `(kx, ky)`, the backward pass:
1. Applies a tilted plane wave: `obj × exp(i(kx·x + ky·y))`
2. Computes Fourier transform and applies pupil mask (optical transfer function)
3. Inverse transforms back to spatial domain
4. Records intensity: `I = |wave|²`

This simulates the effect of different illuminations in a physical FPM system, given a 'gold standard' image (ie. target reconstruction).

### Forward Pass: Reconstruction

Recover the high-resolution complex field from captures (the primary FPM algorithm):

```python
reconstructed = fpm.reconstruct(
    series=image_series,
    output_scale_factor=4,       # Output is 4× larger than input captures
    max_iters=10,                # Number of iterative refinement passes
    pupil_0=None                 # Optional initial pupil estimate
)
```

**Returns**: Complex-valued tensor in spatial domain with shape `(H_out, W_out)` where the output size is either:
- `output_scale_factor × input_size` (if specified)
- Auto-calculated minimum size for full k-space coverage
- Explicitly set via `output_image_size=(H, W)`

#### Reconstruction Algorithm

The implementation uses a **quasi-Newton phase retrieval** approach based on Tian et al. (2014):

1. **Initialization**: First capture initializes the high-res spectrum
2. **Iterative Update Loop**: For each capture at k-vector `(kx, ky)`:
   - Extract relevant Fourier patch from global object estimate
   - Apply pupil function and transform to spatial domain
   - **Intensity constraint**: Replace estimated magnitude with √(measured intensity)
   - **Back-propagate**: Transform corrected wave to Fourier domain
   - **Joint optimization**: Update both object spectrum and pupil using quasi-Newton step
   - **Support constraint**: Enforce binary pupil support

The quasi-Newton update (Tian's method) balances updates between object and pupil:

```python
# Object update
obj_update = pupil* · Δwave / (|pupil|² + α)

# Pupil update
pupil_update = obj* · Δwave / (|obj|² + β)
```

where `Δwave = wave_corrected - wave_estimated` and α, β are regularization parameters.

**Key parameters**:
- `alpha=1.0`: Object update regularization (prevents division by zero)
- `beta=1000.0`: Pupil update regularization (pupil converges slower than object)

### Converting Physical LED Positions

If you have physical LED array coordinates, convert them to k-vectors:

```python
import numpy as np

# LED positions in 3D space (meters), with (0,0,0) at optical axis
led_positions = np.array([
    [0.01, 0.0, 0.05],    # x, y, z for each LED
    [0.0, 0.01, 0.05],
    # ... more LEDs
])

k_vectors = fpm.spatial_to_kvectors(
    led_positions=led_positions,
    wavelength=550e-9,
    pixel_size=1e-6,
    sample_to_lens_distance=0.05,      # 50 mm
    lens_to_sensor_distance=0.50       # 500 mm (10× magnification)
)

# Now use k_vectors with kvectors_to_image_series()
```

## Evaluation & Visualization

Compare reconstruction quality:

```python
from fpm_py import plot_comparison_with_histograms

# Create comparison plots with histograms and Fourier spectra
fig, stats = plot_comparison_with_histograms(
    images=[target, recon_1iter, recon_10iter],
    titles=["Ground Truth", "1 Iteration", "10 Iterations"],
    reference_idx=0  # Use first image as reference for stat calculations
)
plt.show()

# Access quantitative metrics
print(f"SSIM: {stats['ssim']}")
print(f"PSNR: {stats['psnr']}")
```

The `stats` dictionary includes:
- **SSIM**: Structural similarity index
- **PSNR**: Peak signal-to-noise ratio
- **MSE/RMSE**: Mean squared error metrics
- **NCC**: Normalized cross-correlation
- Plus histogram statistics (mean, std, skewness, kurtosis, entropy)

## Implementation Details

### Device Management

The library automatically selects the best available device (CUDA > MPS > CPU):

```python
from fpm_py import best_device

device = best_device()  # Auto-selects GPU if available
series = series.to(device)  # Move data to device
```

### FFT Conventions

All Fourier transforms use **centered** FFTs via `fftshift`/`ifftshift`:

```python
def ft(x):
    return torch.fft.fftshift(torch.fft.fft2(torch.fft.ifftshift(x)))

def ift(x):
    return torch.fft.ifftshift(torch.fft.ifft2(torch.fft.fftshift(x)))
```

This ensures DC component is at the array center, matching the physics convention where k=0 is the optical axis.

### Memory Optimization

- All computations use `torch.no_grad()` (no autograd overhead)
- Float32 precision throughout (GPU-optimized)
- In-place operations where possible (`add_`, `mul_`, `div_`)
- Output dimensions forced to even numbers for FFT efficiency

### Coordinate System

**k-space**: The k-vector `(kx, ky)` represents the **transverse** component of the wavevector, scaled by effective pixel size:

```
k_scaled = (2π/λ) sin(θ) × (pixel_size / magnification)
```

where θ is the illumination angle from the optical axis.

**Fourier patch extraction**: For k-vector `(kx, ky)`, the crop location in the global spectrum is:

```python
x = center_x + int(kx / du)
y = center_y + int(ky / du)
```

This shift-and-multiply theorem maps illumination angles to Fourier space shifts.

## Architecture

```
src/fpm_py/
├── core/
│   ├── structs.py      # Data structures (ImageCapture, ImageSeries)
│   ├── forward.py      # Reconstruction algorithm (forward pass)
│   └── backward.py     # Simulation (backward pass)
├── utils/
│   ├── math_utils.py   # FFT helpers, geometry, overlap operations
│   └── data_utils.py   # Image loading, device selection
└── analysis/
    └── metrics.py      # SSIM, PSNR, visualization
```

**Note**: The naming convention:
- `forward.py` = **reconstruction** (captures → high-res image) - the forward pass
- `backward.py` = **simulation** (high-res image → captures) - the backward pass (inverse of reconstruction)

## References

This implementation is based on:

1. **Tian et al. (2014)** - "Multiplexed coded illumination for Fourier Ptychography with an LED array microscope" - Quasi-Newton optimization
2. **Zheng et al. (2013)** - "Wide-field, high-resolution Fourier ptychographic microscopy" - Original FPM paper
3. **Ou et al. (2015)** - "Embedded pupil function recovery for Fourier ptychographic microscopy" - Pupil recovery method

For the mathematical foundations of Fourier ptychography, see: https://en.wikipedia.org/wiki/Fourier_ptychography

## Example: Full Pipeline

See [`main.py`](main.py) for a complete working example that:
1. Loads a test image (USAF resolution target)
2. Generates a 5×5 grid of k-vectors
3. Simulates FPM captures
4. Reconstructs with 1 and 10 iterations
5. Visualizes results with comparison metrics

## License

MIT License - See LICENSE file for details.

## Contributing

Issues and pull requests welcome at: https://github.com/rspcunningham/fpm-py

---

**Version**: 2.0.0 | **Author**: Robin Cunningham

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "fpm-py",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<3.13,>=3.9",
    "maintainer_email": null,
    "keywords": "fourier, ptychography, microscopy, imaging, reconstruction",
    "author": null,
    "author_email": "Robin Cunningham <rspcunningham@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/66/b3/eb70eae208493cbb337e4eb68430a97d3e5fa342d4c3f90c4d5c07674443/fpm_py-2.0.1.tar.gz",
    "platform": null,
    "description": "# fpm-py\n\nA Python library for Fourier Ptychographic Microscopy (FPM) simulation and reconstruction with GPU acceleration.\n\n## Overview\n\nThis library implements both the **forward pass** (reconstruction from captures) and **backward pass** (simulation of captures from an object) for Fourier ptychography. It uses PyTorch for efficient GPU-accelerated computation and provides a clean, type-safe API for research and development.\n\nFourier ptychography synthesizes a high-resolution, wide-field image by computationally combining multiple low-resolution images captured under varying illumination angles. This creates a synthetic aperture larger than the physical objective NA, enabling resolution enhancement while maintaining a large field of view.\n\n## Installation\n\n```bash\npip install fpm-py\n```\n\nOr with `uv` (preferred):\n```bash\nuv add fpm-py\n```\n\n**Requirements**: 3.12 \u2265 Python \u2265 3.9, PyTorch \u2265 2.8.0\n\n## Quick Start\n\n```python\nimport fpm_py as fpm\nfrom fpm_py.utils.data_utils import image_to_tensor\nimport torch\n\n# Load a test object\nobj = image_to_tensor(\"path/to/image.png\", to_complex=True)\n\n# Define k-vectors (illumination angles) - 5\u00d75 grid\ngrid_size = 5\nspacing = 0.2\nk_range = torch.linspace(-(grid_size // 2), grid_size // 2, grid_size) * spacing\nkx, ky = torch.meshgrid(k_range, k_range, indexing=\"ij\")\nk_vectors = torch.stack((kx.flatten(), ky.flatten()), dim=1)\n\n# Simulate FPM captures (backward pass)\ndataset = fpm.kvectors_to_image_series(\n    obj=obj,\n    k_vectors=k_vectors,\n    pupil_radius=100,\n    wavelength=550e-9,      # 550 nm (green light)\n    pixel_size=1e-6,        # 1 \u03bcm pixels\n    magnification=10.0      # 10\u00d7 objective\n)\n\n# Reconstruct high-resolution image (forward pass)\nreconstruction = fpm.reconstruct(dataset, output_scale_factor=4, max_iters=10)\n\n# View magnitude\nimport matplotlib.pyplot as plt\nplt.imshow(reconstruction.abs().cpu().numpy(), cmap='gray')\nplt.show()\n```\n\nFor a more detailed walkthrough, see [main.py](https://github.com/rspcunningham/fpm-py/blob/main/main.py).\n\n## Core Concepts\n\n### Data Structures\n\n#### `ImageCapture`\nRepresents a single FPM capture with its associated k-vector:\n```python\n@dataclass(frozen=True)\nclass ImageCapture:\n    image: torch.Tensor      # (H, W) intensity image\n    k_vector: torch.Tensor   # (2,) illumination angle in k-space\n```\n\n#### `ImageSeries`\nContainer for multiple captures with acquisition settings:\n```python\n@dataclass\nclass ImageSeries:\n    captures: Sequence[ImageCapture]\n    settings: AcquisitionSettings  # du, wavelength, pixel_size\n    device: torch.device\n```\n\nThe `du` parameter is the **k-space lattice spacing** \u2014 it relates pixel coordinates in Fourier space to physical spatial frequencies. Automatically computed as:\n\n```python\ndu = wavelength / (N \u00d7 effective_pixel_size)\n```\n\nwhere `effective_pixel_size = pixel_size / magnification`.\n\n### Backward Pass: Simulation\n\nGenerate synthetic FPM captures from a known object (inverse of reconstruction):\n\n```python\nseries = fpm.kvectors_to_image_series(\n    obj=complex_field,           # Complex object (amplitude + phase)\n    k_vectors=k_vectors,         # (N, 2) illumination directions\n    pupil_radius=100,            # Pupil radius in pixels (related to NA)\n    wavelength=550e-9,           # Illumination wavelength [m]\n    pixel_size=1e-6,             # Camera pixel size [m]\n    magnification=10.0,          # Optical magnification\n    pupil=None                   # Optional custom pupil function\n)\n```\n\n**Physics**: For each k-vector `(kx, ky)`, the backward pass:\n1. Applies a tilted plane wave: `obj \u00d7 exp(i(kx\u00b7x + ky\u00b7y))`\n2. Computes Fourier transform and applies pupil mask (optical transfer function)\n3. Inverse transforms back to spatial domain\n4. Records intensity: `I = |wave|\u00b2`\n\nThis simulates the effect of different illuminations in a physical FPM system, given a 'gold standard' image (ie. target reconstruction).\n\n### Forward Pass: Reconstruction\n\nRecover the high-resolution complex field from captures (the primary FPM algorithm):\n\n```python\nreconstructed = fpm.reconstruct(\n    series=image_series,\n    output_scale_factor=4,       # Output is 4\u00d7 larger than input captures\n    max_iters=10,                # Number of iterative refinement passes\n    pupil_0=None                 # Optional initial pupil estimate\n)\n```\n\n**Returns**: Complex-valued tensor in spatial domain with shape `(H_out, W_out)` where the output size is either:\n- `output_scale_factor \u00d7 input_size` (if specified)\n- Auto-calculated minimum size for full k-space coverage\n- Explicitly set via `output_image_size=(H, W)`\n\n#### Reconstruction Algorithm\n\nThe implementation uses a **quasi-Newton phase retrieval** approach based on Tian et al. (2014):\n\n1. **Initialization**: First capture initializes the high-res spectrum\n2. **Iterative Update Loop**: For each capture at k-vector `(kx, ky)`:\n   - Extract relevant Fourier patch from global object estimate\n   - Apply pupil function and transform to spatial domain\n   - **Intensity constraint**: Replace estimated magnitude with \u221a(measured intensity)\n   - **Back-propagate**: Transform corrected wave to Fourier domain\n   - **Joint optimization**: Update both object spectrum and pupil using quasi-Newton step\n   - **Support constraint**: Enforce binary pupil support\n\nThe quasi-Newton update (Tian's method) balances updates between object and pupil:\n\n```python\n# Object update\nobj_update = pupil* \u00b7 \u0394wave / (|pupil|\u00b2 + \u03b1)\n\n# Pupil update\npupil_update = obj* \u00b7 \u0394wave / (|obj|\u00b2 + \u03b2)\n```\n\nwhere `\u0394wave = wave_corrected - wave_estimated` and \u03b1, \u03b2 are regularization parameters.\n\n**Key parameters**:\n- `alpha=1.0`: Object update regularization (prevents division by zero)\n- `beta=1000.0`: Pupil update regularization (pupil converges slower than object)\n\n### Converting Physical LED Positions\n\nIf you have physical LED array coordinates, convert them to k-vectors:\n\n```python\nimport numpy as np\n\n# LED positions in 3D space (meters), with (0,0,0) at optical axis\nled_positions = np.array([\n    [0.01, 0.0, 0.05],    # x, y, z for each LED\n    [0.0, 0.01, 0.05],\n    # ... more LEDs\n])\n\nk_vectors = fpm.spatial_to_kvectors(\n    led_positions=led_positions,\n    wavelength=550e-9,\n    pixel_size=1e-6,\n    sample_to_lens_distance=0.05,      # 50 mm\n    lens_to_sensor_distance=0.50       # 500 mm (10\u00d7 magnification)\n)\n\n# Now use k_vectors with kvectors_to_image_series()\n```\n\n## Evaluation & Visualization\n\nCompare reconstruction quality:\n\n```python\nfrom fpm_py import plot_comparison_with_histograms\n\n# Create comparison plots with histograms and Fourier spectra\nfig, stats = plot_comparison_with_histograms(\n    images=[target, recon_1iter, recon_10iter],\n    titles=[\"Ground Truth\", \"1 Iteration\", \"10 Iterations\"],\n    reference_idx=0  # Use first image as reference for stat calculations\n)\nplt.show()\n\n# Access quantitative metrics\nprint(f\"SSIM: {stats['ssim']}\")\nprint(f\"PSNR: {stats['psnr']}\")\n```\n\nThe `stats` dictionary includes:\n- **SSIM**: Structural similarity index\n- **PSNR**: Peak signal-to-noise ratio\n- **MSE/RMSE**: Mean squared error metrics\n- **NCC**: Normalized cross-correlation\n- Plus histogram statistics (mean, std, skewness, kurtosis, entropy)\n\n## Implementation Details\n\n### Device Management\n\nThe library automatically selects the best available device (CUDA > MPS > CPU):\n\n```python\nfrom fpm_py import best_device\n\ndevice = best_device()  # Auto-selects GPU if available\nseries = series.to(device)  # Move data to device\n```\n\n### FFT Conventions\n\nAll Fourier transforms use **centered** FFTs via `fftshift`/`ifftshift`:\n\n```python\ndef ft(x):\n    return torch.fft.fftshift(torch.fft.fft2(torch.fft.ifftshift(x)))\n\ndef ift(x):\n    return torch.fft.ifftshift(torch.fft.ifft2(torch.fft.fftshift(x)))\n```\n\nThis ensures DC component is at the array center, matching the physics convention where k=0 is the optical axis.\n\n### Memory Optimization\n\n- All computations use `torch.no_grad()` (no autograd overhead)\n- Float32 precision throughout (GPU-optimized)\n- In-place operations where possible (`add_`, `mul_`, `div_`)\n- Output dimensions forced to even numbers for FFT efficiency\n\n### Coordinate System\n\n**k-space**: The k-vector `(kx, ky)` represents the **transverse** component of the wavevector, scaled by effective pixel size:\n\n```\nk_scaled = (2\u03c0/\u03bb) sin(\u03b8) \u00d7 (pixel_size / magnification)\n```\n\nwhere \u03b8 is the illumination angle from the optical axis.\n\n**Fourier patch extraction**: For k-vector `(kx, ky)`, the crop location in the global spectrum is:\n\n```python\nx = center_x + int(kx / du)\ny = center_y + int(ky / du)\n```\n\nThis shift-and-multiply theorem maps illumination angles to Fourier space shifts.\n\n## Architecture\n\n```\nsrc/fpm_py/\n\u251c\u2500\u2500 core/\n\u2502   \u251c\u2500\u2500 structs.py      # Data structures (ImageCapture, ImageSeries)\n\u2502   \u251c\u2500\u2500 forward.py      # Reconstruction algorithm (forward pass)\n\u2502   \u2514\u2500\u2500 backward.py     # Simulation (backward pass)\n\u251c\u2500\u2500 utils/\n\u2502   \u251c\u2500\u2500 math_utils.py   # FFT helpers, geometry, overlap operations\n\u2502   \u2514\u2500\u2500 data_utils.py   # Image loading, device selection\n\u2514\u2500\u2500 analysis/\n    \u2514\u2500\u2500 metrics.py      # SSIM, PSNR, visualization\n```\n\n**Note**: The naming convention:\n- `forward.py` = **reconstruction** (captures \u2192 high-res image) - the forward pass\n- `backward.py` = **simulation** (high-res image \u2192 captures) - the backward pass (inverse of reconstruction)\n\n## References\n\nThis implementation is based on:\n\n1. **Tian et al. (2014)** - \"Multiplexed coded illumination for Fourier Ptychography with an LED array microscope\" - Quasi-Newton optimization\n2. **Zheng et al. (2013)** - \"Wide-field, high-resolution Fourier ptychographic microscopy\" - Original FPM paper\n3. **Ou et al. (2015)** - \"Embedded pupil function recovery for Fourier ptychographic microscopy\" - Pupil recovery method\n\nFor the mathematical foundations of Fourier ptychography, see: https://en.wikipedia.org/wiki/Fourier_ptychography\n\n## Example: Full Pipeline\n\nSee [`main.py`](main.py) for a complete working example that:\n1. Loads a test image (USAF resolution target)\n2. Generates a 5\u00d75 grid of k-vectors\n3. Simulates FPM captures\n4. Reconstructs with 1 and 10 iterations\n5. Visualizes results with comparison metrics\n\n## License\n\nMIT License - See LICENSE file for details.\n\n## Contributing\n\nIssues and pull requests welcome at: https://github.com/rspcunningham/fpm-py\n\n---\n\n**Version**: 2.0.0 | **Author**: Robin Cunningham\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Fourier ptychography tooling for simulation and reconstruction",
    "version": "2.0.1",
    "project_urls": {
        "Homepage": "https://github.com/rspcunningham/fpm-py",
        "Issues": "https://github.com/rspcunningham/fpm-py/issues",
        "Repository": "https://github.com/rspcunningham/fpm-py"
    },
    "split_keywords": [
        "fourier",
        " ptychography",
        " microscopy",
        " imaging",
        " reconstruction"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "09f19ef59f2427bae9b1d53c5318e3a6d0236f4d81d6e552a66d7d807094df58",
                "md5": "514df436841f648c4927f4e16021fd80",
                "sha256": "6a2708de8c48951b5e4d340046a1ca2237ba06264fa3e94a185d9562829e688d"
            },
            "downloads": -1,
            "filename": "fpm_py-2.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "514df436841f648c4927f4e16021fd80",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<3.13,>=3.9",
            "size": 21890,
            "upload_time": "2025-10-08T04:38:40",
            "upload_time_iso_8601": "2025-10-08T04:38:40.667025Z",
            "url": "https://files.pythonhosted.org/packages/09/f1/9ef59f2427bae9b1d53c5318e3a6d0236f4d81d6e552a66d7d807094df58/fpm_py-2.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "66b3eb70eae208493cbb337e4eb68430a97d3e5fa342d4c3f90c4d5c07674443",
                "md5": "180ea8910bead7a1782cb3e5db28ca08",
                "sha256": "c8d55e4b45392fd5b9b9610bcae7fa167e8057a88d528f8919ba6c090a1a83a2"
            },
            "downloads": -1,
            "filename": "fpm_py-2.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "180ea8910bead7a1782cb3e5db28ca08",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<3.13,>=3.9",
            "size": 23674,
            "upload_time": "2025-10-08T04:38:42",
            "upload_time_iso_8601": "2025-10-08T04:38:42.079077Z",
            "url": "https://files.pythonhosted.org/packages/66/b3/eb70eae208493cbb337e4eb68430a97d3e5fa342d4c3f90c4d5c07674443/fpm_py-2.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-08 04:38:42",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "rspcunningham",
    "github_project": "fpm-py",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "fpm-py"
}
        
Elapsed time: 1.07888s