Metadata-Version: 2.4
Name: superpixel
Version: 1.0.0
Summary: Superpixel PSF, ensquared-energy, FWHM, and MTF analysis with a NiceGUI interface
Author: L. Riegger
License-Expression: MIT
License-File: LICENSE
Requires-Dist: nicegui[native]>=3.0,<4
Requires-Dist: numpy>=1.24
Requires-Dist: plotly>=5.18
Requires-Dist: scipy>=1.10
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/himbeles/superpixel
Project-URL: Repository, https://github.com/himbeles/superpixel
Project-URL: License, https://github.com/himbeles/superpixel/blob/main/LICENSE
Provides-Extra: dev
Description-Content-Type: text/markdown

# Superpixel

[GitHub repository](https://github.com/himbeles/superpixel)

`superpixel` models the effective point-spread function (PSF) of a detector
superpixel. It combines an optical PSF with a gain-weighted array of shifted
square-subpixel responses, or models the complete detector response as a
single point at the superpixel origin. 
It handles different subpixel gains or dead subpixels.

The package calculates:

- ensquared energy in a rectangular window or encircled energy in a circular
  aperture;
- the radiometric barycenter;
- orthogonal PSF cuts through the barycenter and their FWHM values;
- the normalized 2D MTF, its x/y cuts, and values at the tiled-superpixel
  Nyquist frequencies.

Superpixel requires Python 3.10 or newer and uses
[uv](https://docs.astral.sh/uv/) for dependency management.

## Python API

From a source checkout, create the environment and install the locked
dependencies:

```bash
uv sync
```

### Basic analysis

A typical analysis creates an optical PSF, constructs the sampled
superpixel response, and evaluates all metrics:

```python
import numpy as np

from superpixel import (
    IntegrationWindow,
    analyze_superpixel,
    build_superpixel_psf,
    gaussian_psf,
)

psf = build_superpixel_psf(
    gaussian_psf(fwhm_x=14.4, fwhm_y=12.0),
    shape=(2, 3),
    pitch=(10.0, 10.0),
    subpixel_side=9.0,
    gains=np.array(
        [
            [1.00, 0.95, 1.05],
            [0.90, 1.10, 1.00],
        ]
    ),
    model_half_width=(80.0, 80.0),
    sample_spacing=(0.2, 0.2),
)

analysis = analyze_superpixel(
    psf,
    IntegrationWindow(
        center=(0.0, 0.0),
        size=(30.0, 20.0),
    ),
)

print("Integrated energy:", analysis.ensquared_energy)
print("Barycenter:", analysis.barycenter)
print("FWHM:", analysis.cuts.fwhm_x.width, analysis.cuts.fwhm_y.width)
print(
    "MTF at Nyquist:",
    analysis.mtf.at_nyquist_x,
    analysis.mtf.at_nyquist_y,
)
```

`build_superpixel_psf` returns a `SuperpixelPSF` whose sampled density is
normalized so that its integral over the numerical grid is one.
`output_grid_energy_fraction` is the fraction of the internally normalized,
gain-weighted effective PSF that lies inside the requested output grid before
this final normalization. It depends on relative gains when shifted subpixel
responses are cropped by different amounts. Multiplying all gains by the same
factor has no effect. Increase `model_half_width` until the results converge,
particularly for an Airy PSF.

`analyze_superpixel` returns the PSF, integrated energy, barycenter,
barycenter-centered PSF cuts and FWHM values, and the complete 2D MTF with its
cuts.

### Coordinate and unit conventions

- Coordinate pairs and sizes are `(x, y)` = `(column, row)`.
- Array shapes and gain matrices are `(rows, columns)`.
- `pitch=(pitch_x, pitch_y)`, `model_half_width=(x, y)`, and
  `sample_spacing=(x, y)`.
- All lengths in one analysis must use the same unit.
- The tiled-superpixel Nyquist frequencies are
  `1 / (2 * columns * pitch_x)` and `1 / (2 * rows * pitch_y)`.
- A full-superpixel point response has no detector pitch and therefore no
  detector-defined Nyquist frequency.

### Optical PSF models

A Gaussian can be specified using sigma or FWHM values. Its optional rotation
angle is measured counter-clockwise in radians:

```python
from superpixel import gaussian_psf

model = gaussian_psf(fwhm_x=14.4, fwhm_y=10.0, angle=0.2)
```

An Airy PSF accepts either its first-zero radius or wavelength and f-number:

```python
from superpixel import airy_psf

model = airy_psf(wavelength=0.55, f_number=8.0)
# Alternatively: airy_psf(first_zero_radius=5.37)
```

Wavelength and first-zero radius must use the same length unit as the
detector geometry.

Any vectorized callable with the signature `psf(x, y)` can be supplied:

```python
import numpy as np


def custom_psf(x, y):
    sigma_x = 5.0
    sigma_y = 7.0
    return np.exp(-0.5 * ((x / sigma_x) ** 2 + (y / sigma_y) ** 2))
```

The callable receives NumPy arrays and must return finite, non-negative
values. It does not need to be pre-normalized.

For trusted local code stored as text, `custom_psf_from_code` compiles a
function named `psf`. This executes Python and is not a security sandbox.

### Optical and detector response modes

Pass `None` as the optical PSF to model ideal optics with only the square
detector response:

```python
pixel_only = build_superpixel_psf(
    None,
    subpixel_side=10.0,
)
```

Square-subpixel mode is the default and requires a positive
`subpixel_side`. There is no per-subpixel point-response mode.

Use `detector_response="point"` to replace the complete subpixel array with a
single point response at the origin. Shape, pitch, side length, and gains are
ignored in this mode:

```python
optics_only = build_superpixel_psf(
    gaussian_psf(fwhm_x=14.4),
    detector_response="point",
)

sampled_dirac = build_superpixel_psf(
    None,
    detector_response="point",
)
```

Numerical convolution is performed only when both an optical PSF and the
square detector response are enabled.

### Integration apertures

The default `IntegrationWindow` is rectangular. Use a circular aperture by
selecting its shape and radius:

```python
window = IntegrationWindow(
    center=(0.0, 0.0),
    shape="circle",
    radius=15.0,
)
analysis = analyze_superpixel(psf, window)
```

### JSON configuration

The UI configuration schema is also available through the Python API:

```python
from superpixel import AppConfig, load_config, save_config

config = AppConfig()
save_config(config, "superpixel-config.json")
restored = load_config("superpixel-config.json")
```

Configuration objects are validated when constructed or loaded. Invalid
values and unsupported configuration versions raise `ValueError`.

## Desktop UI

Start the native NiceGUI application from the repository:

```bash
uv run superpixel
```

The repository entry point can also be run directly:

```bash
uv run python run_ui.py
```

The input area is organized into Optical PSF, Superpixel Configuration,
Numerical Grid, and Integrated Energy Analysis sections. Click **Simulate**
to run the same numerical API used directly from Python and update the PSF,
MTF, cut plots, and numerical results.

The configuration menu provides:

- **Save configuration** to download the current settings as JSON;
- **Load configuration** to validate and apply a JSON configuration to the
  inputs;
- **Reset to defaults** to restore the original application settings.

Loading a configuration updates the inputs but does not execute custom PSF
code or start a simulation. Review the settings and click **Simulate**.

Every simulation also saves the current configuration to
`~/.superpixel/config.json`. The UI restores this file on its next launch.
Set `SUPERPIXEL_CONFIG_PATH` to use a different automatic configuration
path.

Custom PSF code entered in the UI is executed as Python. Only run code you
trust, and do not expose the UI as a public service while custom code is
enabled.

## Maintenance and contribution

### Development workflow

Synchronize the locked environment after cloning the repository or changing
dependencies:

```bash
uv sync
```

Run the complete test suite before submitting a change:

```bash
uv run pytest
```

When dependencies change, update `pyproject.toml` and refresh `uv.lock` with
the appropriate `uv add`, `uv remove`, or `uv lock` command. Commit both files
so development and packaged builds use the same resolved dependencies.

The main source areas are:

- `src/superpixel/simulation.py`: sampled PSF construction and detector
  response modes;
- `src/superpixel/models.py`: built-in and custom optical PSF models;
- `src/superpixel/metrics.py`: energy, barycenter, cuts, FWHM, and MTF;
- `src/superpixel/analysis.py`: high-level analysis orchestration;
- `src/superpixel/plotting.py`: Plotly figures;
- `src/superpixel/config.py`: validated JSON configuration;
- `src/superpixel/ui.py`: the NiceGUI wrapper;
- `tests/`: numerical, configuration, plotting, and UI tests.

Keep numerical behavior in the Python API rather than in the UI. The UI
should remain a thin wrapper that builds validated inputs, calls the analysis
functions, and presents their results. Export intended public interfaces from
`src/superpixel/__init__.py` and add tests for changed behavior.

### Build a one-file executable

The application uses NiceGUI native mode and can be packaged with PyInstaller.
Build on the same operating system and CPU architecture on which the
executable will run; PyInstaller does not cross-compile.

After `uv sync`, run from the repository root:

```bash
uv run build
```

The `build` command reads the installed package version and passes
`Superpixel-<version>` to `nicegui-pack`. Updating the project version
therefore also updates the executable name without changing the build
command. Run it from the repository root so it can package `run_ui.py`.

The executable is written to:

- Windows: `dist/Superpixel-<version>.exe`
- macOS: `dist/Superpixel-<version>`
- Linux: `dist/Superpixel-<version>`

## License

Superpixel is released under the [MIT License](LICENSE).
