Metadata-Version: 2.4
Name: lighthit
Version: 0.2.0a5
Summary: Spectral radiative-transfer response for neutrino-telescope optical modules
Author: Dmitry Naumov
License: BSD-3-Clause
Project-URL: Repository, https://github.com/NeutrinoHit/LightHit
Keywords: radiative transfer,neutrino telescope,Cherenkov,Baikal-GVD
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.26
Requires-Dist: scipy>=1.11
Provides-Extra: accelerate
Requires-Dist: numba>=0.61; extra == "accelerate"
Provides-Extra: notebook
Requires-Dist: jupyterlab>=4; extra == "notebook"
Requires-Dist: ipykernel>=6; extra == "notebook"
Requires-Dist: nbformat>=5; extra == "notebook"
Requires-Dist: nbclient>=0.10; extra == "notebook"
Requires-Dist: nbconvert>=7; extra == "notebook"
Requires-Dist: matplotlib>=3.8; extra == "notebook"
Requires-Dist: plotly>=5; extra == "notebook"
Provides-Extra: viewer
Requires-Dist: plotly>=5; extra == "viewer"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: build>=1; extra == "dev"
Dynamic: license-file

# LightHit

LightHit computes the expected optical-module response to light propagating in
a homogeneous scattering medium. It solves the time-dependent radiative
transfer equation (RTE), including unscattered, once-scattered and
multiply-scattered light, and returns expected photoelectrons per module and
per time bin.

The medium, detector and source are ordinary Python objects. There is no global
initialisation state, so several configurations can safely coexist in one
program.

LightHit is currently an alpha release. Validate numerical settings for your
medium and geometry before using its results in an analysis.

## Installation

```bash
python -m pip install lighthit
```

Numba acceleration is strongly recommended for tracks, showers and large
detector arrays:

```bash
python -m pip install 'lighthit[accelerate]'
```

Python 3.11 or newer is required.

## Quick start

The example below defines a spectral medium, two optical modules and a
monochromatic isotropic flash.

```python
import numpy as np
import lighthit as lh

# Optical properties tabulated versus wavelength.
medium = lh.SpectralMedium(
    wavelength_nm=[400.0, 450.0, 500.0],
    absorption_per_m=[0.030, 0.020, 0.040],
    scattering_per_m=[0.030, 0.022, 0.017],
    phase_index=[1.344, 1.339, 1.336],
    group_index=[1.386, 1.374, 1.367],
    g=0.9,
)

# Response functions receive NumPy arrays and return arrays of the same shape.
def angular_acceptance(head_on_cosine):
    return np.ones_like(np.asarray(head_on_cosine, dtype=float))

def spectral_efficiency(wavelength_nm):
    return np.full_like(np.asarray(wavelength_nm, dtype=float), 0.20)

detector = lh.DetectorArray(
    positions_m=[[20.0, 0.0, 0.0], [35.0, 0.0, 0.0]],
    orientations=[[-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]],
    effective_area_m2=0.05,
    angular_acceptance=angular_acceptance,
    spectral_efficiency=spectral_efficiency,
)

source = lh.IsotropicFlash.monochromatic(
    position_m=[0.0, 0.0, 0.0],
    photons=1.0e8,
    wavelength_nm=450.0,
)

# build() returns a reusable kernel and prepares only the tables this source
# needs. The same kernel can then transport more compatible events.
kernel = lh.build(medium, detector, source)
response = kernel.transport(source)

print(response.charge_pe)   # integrated expected photoelectrons, one per OM
print(response.bins_pe)     # expected photoelectrons, shape (OM, time bin)
```

The first build may take noticeably longer than later event calculations.
Set `KernelConfig(cache_directory=...)` to reuse transport tables across
processes.

## Sources

The high-level dispatcher accepts:

```python
flash = lh.IsotropicFlash.monochromatic(
    position_m=[0, 0, 0], photons=1e8, wavelength_nm=450
)

track = lh.CherenkovTrack(
    start_m=[0, 0, -20],
    direction=[0.2, 0.1, 0.97],
    length_m=40,
    beta=1.0,
)

shower = lh.G4Shower.from_hdf5("event.h5", event=0)
```

For tracks and showers, `kernel.transport(source)` automatically selects the
source engine and uses the detector's angular response. Research methods are
never selected unless explicitly enabled.

## Configuring the calculation

Numerical settings are collected in `KernelConfig`:

```python
config = lh.KernelConfig(
    wavelength_nodes=9,
    threshold_pe=0.01,
    cache_directory="lighthit-cache",
)

kernel = lh.build(medium, detector, track, config=config)
response = kernel.transport(track)
```

Important inputs are explicit:

- `SpectralMedium` contains absorption and scattering coefficients in m⁻¹,
  phase and group refractive indices, and the Henyey–Greenstein parameter `g`.
- `DetectorArray` contains OM positions, orientations, effective areas,
  angular acceptance and wavelength-dependent detection efficiency.
- A detector orientation points from the OM toward a head-on source;
  `angular_acceptance(+1)` is the head-on response.
- `spectral_efficiency` should include all wavelength-dependent detection
  factors required by the application, such as quantum efficiency and optical
  transmission.

## Reading the response

`kernel.transport(...)` returns a `TransportResponse`.

| Attribute | Meaning |
|---|---|
| `charge_pe` | Integrated expected photoelectrons, shape `(OM,)` |
| `bins_pe` | Expected photoelectrons per relative time bin, shape `(OM, bin)` |
| `charge_components_pe` | Integrated contributions `[ballistic, one, two-or-more]` |
| `components_pe` | The same three components per time bin |
| `active` | OMs for which the full time spectrum was evaluated |
| `relative_time_edges_ns` | Bin edges relative to each OM's time origin |
| `time_origin_ns` | One absolute time origin per OM |
| `metadata` | Method, backend and numerical diagnostics |

Absolute bin edges for every module are

```python
absolute_edges_ns = (
    response.time_origin_ns[:, None]
    + response.relative_time_edges_ns[None, :]
)
```

The integrated charge is evaluated independently at zero frequency. A finite
time window and a finite frequency grid mean that `bins_pe.sum(axis=1)` need
not equal `charge_pe` exactly. Signed ringing is reported rather than silently
clipped or renormalised.

Responses can be stored as compressed NumPy files:

```python
response.save("response.npz")
```

## Interactive viewer

Install the optional viewer dependency:

```bash
python -m pip install 'lighthit[viewer]'
```

Turn a response into a standalone interactive HTML file:

```python
payload = lh.viewer_payload(
    response,
    source=source,
    event_id="flash-450nm",
    label="450 nm calibration flash",
)

viewer_path = lh.write_event_viewer(payload, "lighthit-viewer.html")
print(viewer_path)
```

Open `lighthit-viewer.html` in a web browser. It contains the detector geometry,
integrated charge by scattering order, a selectable per-OM time histogram and a
time animation. A portable `lighthit-viewer.json` companion is written beside
the HTML file.

## Scope

The current model assumes a homogeneous, unbounded medium with elastic
Henyey–Greenstein scattering. It does not include boundaries, layered media,
structural shadowing, polarisation, detector electronics, trigger or noise.

## License

LightHit is distributed under the BSD 3-Clause License.
