Metadata-Version: 2.5
Name: lasercalc
Version: 0.1.1
Summary: IEC 60825-1 laser safety classification, MPE, NOHD and hazard calculations
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: reports
Requires-Dist: jinja2>=3.1; extra == 'reports'
Description-Content-Type: text/markdown

# Lasercalc

> ### ⚠️ Alpha
>
> lasercalc is alpha software and has not yet been validated by the scientific community or industry. It's an independent implementation of parts of IEC 60825-1. It's useful for exploration and as an input to a review, but please treat the standard itself as the authority and have a qualified laser safety officer check anything it produces. v1.0 will follow once the package has been reviewed and validated.

IEC 60825-1 laser safety classification, MPE, NOHD and hazard calculations as a pure-Python, dependency-free library. The core is plain frozen dataclasses and functions with no I/O, so it drops into a web service, CLI, notebook, desktop app or CI job without adaptation.

Requires Python 3.10+.

## Why

Laser classification work tends to live in spreadsheets, PDFs and proprietary desktop tools. None of those embed in software, and none of them show you how they arrived at a number. lasercalc exists to make the calculation a library call that you can read, test and audit.

- **Auditable, not oracular.** `classify()` returns every criterion it evaluated: the AEL, the measured value, and the pass/fail. A result can be checked line by line instead of taken on faith.
- **Honest about its limits.** Every deliberate approximation is marked in the source with a `Simplification:` comment and listed in a table below, with the direction of the error. Nothing conservative is silently sold as exact.
- **Zero dependencies.** The core imports only the standard library, so it installs anywhere, adds no supply-chain surface, and does not go stale when a numeric stack moves on.
- **Embeddable.** No I/O, no globals, no config files. Frozen dataclasses in, frozen dataclasses out, so `dataclasses.asdict()` is enough to serve it over an API or diff two results in a test.
- **Reproducible.** Same inputs, same numbers, on any machine, forever. A classification you ran last year can be re-run and compared today.
- **Reportable.** Ships a self-contained HTML classification report for the paperwork side of the job.

## Install

```bash
pip install lasercalc
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add lasercalc
```

## Usage

```python
from lasercalc import LaserSource, classify, nohd, required_od
from lasercalc.reporting import render_html

src = LaserSource(
    wavelength_nm=532,
    power_w=5e-3,
    beam_diameter_m=1e-3,
    divergence_rad=1e-3,
    label="Green pointer",
)

result = classify(src)
result.laser_class      # "3R"
result.label_text       # mandated warning label wording
result.checks           # full audit trail: every AEL evaluated, pass/fail

n = nohd(src)           # -> 14.8 m
od = required_od(src)   # -> OD 2.3

html = render_html(result, nohd=n, od=od, organisation="Acme Photonics")
# PDF: weasyprint.HTML(string=html).write_pdf()
```

Everything returned is a frozen dataclass, so `dataclasses.asdict()` is enough to serialise a result for JSON APIs, and `LaserSource` can be built directly from any validated input model.

## Public API

| Import | Purpose |
| --- | --- |
| `LaserSource`, `EmissionMode` | Validated source description in SI units |
| `classify` → `ClassificationResult`, `CriterionCheck` | Class determination with a per-criterion audit trail |
| `mpe_eye` → `MpeResult` | Maximum permissible exposure at the eye |
| `nohd` → `NohdResult` | Nominal ocular hazard distance |
| `required_od` → `OdResult` | Minimum eyewear optical density |
| `irradiance_w_m2` | Irradiance at a given distance |
| `correction_factors` | C1–C7, T1, T2 |
| `lasercalc.reporting.render_html` | Self-contained HTML classification report |

## v0.1 scope (read before trusting outputs)

**Implemented:**

- CW classification (Classes 1, 2, 3R, 3B, 4), Condition 3 (naked eye), point and small extended sources (C6 scaling)
- MPE (eye): visible dual photochemical/thermal limits, NIR retinal (700–1400 nm), far-IR (>1400 nm), simplified UV (302.5–400 nm)
- Correction factors C1–C7, T1, T2; C5 (2014 rules, simplified) staged for the pulsed engine
- NOHD, required eyewear OD, irradiance at distance, Gaussian peak irradiance
- Self-contained HTML classification report (stdlib only; PDF conversion is left to the caller)

**Not yet implemented (raises or documented as simplified):**

- Pulsed / pulse-train classification (three-criteria evaluation with C5)
- Classes 1M / 2M / 1C, which need Condition 1 (telescope) measurement; current results assume the whole beam fits a 7 mm pupil, which is worst-case for collimated beams
- Skin MPEs, 180–302.5 nm band, EN 207 LB rating mapping
- Full UV dual-limit tables; sub-ns regimes use conservative placeholders

**Validation status: NOT validated for regulatory use.** Tests pin the engine to well-known reference values (25.46 W/m² visible aversion MPE, 0.39/1/5 mW pointer class boundaries, 50 W/m² at 1064 nm, ~15 m pointer NOHD). Treat every output as input to a competent person's assessment, not as a substitute for one. Reports carry a disclaimer to that effect.

## Modelling simplifications

Deliberate corners cut in v0.1, marked in the source with `Simplification:` comments. Each names its ceiling and upgrade path.

| Where | Simplification | Direction |
| --- | --- | --- |
| `correction_factors.c6` | α_max fixed at 100 mrad; the 2014 edition makes it time-dependent for pulsed sources | Exact for CW |
| `correction_factors.c5` | Point-source rule only; extended-source refinements (α > 5 mrad) omitted | Conservative |
| `mpe` visible & NIR, t < 100 ps | Flat placeholder radiant exposure (1.5e-4 J/m² · C6) instead of the Table A.1 short-pulse rows | Unverified, do not rely on |
| `mpe` NIR, t < 50 µs | One plateau for the whole 700–1400 nm band; strictly 13 µs for 700–1050 nm | Conservative below 1050 nm |
| `mpe` far IR, 1 ns ≤ t < 10 s | Single 5600·t^0.25 J/m² curve for the whole band | **Non-conservative** at 1400–1500 and 1800–2600 nm, where Table A.1 is lower |
| `mpe` far IR, t < 1 ns | Flat 100 J/m² placeholder | Unverified, do not rely on |
| `mpe` UV, 302.5–400 nm | Dominant 30·C2 J/m² photochemical limit compared against the C1 thermal limit, rather than the full dual-limit tables | Verify vs Table A.1 |
| `ael.ael_class1_cw_w` | UV and far-IR AELs derived as MPE × aperture rather than read from the explicit tables | Verify vs Tables 3–8 |
| `ael.ael_class3b_cw_w` | UV (<315 nm) 3B AEL flattened to 30 mW | Verify vs Table 8 |
| `hazards.beam_diameter_m` | Far-field linear growth d(z) = d₀ + φ·z; no Rayleigh-range treatment | Standard NOHD practice |
| `hazards.irradiance_w_m2` | Top-hat beam profile; a Gaussian peak is ~2× higher | Matches the standard's aperture-averaged measurement |

Correction factors return 1.0 outside their applicable wavelength range, by convention, since the tables that would use them do not apply there.

## Roadmap

1. Pulsed engine: single-pulse / average / pulse-train criteria with C5, worst-case duration search
2. Condition 1 evaluation → 1M/2M determination
3. EN 207 (LB ratings) and EN 208 mapping from OD
4. Golden test corpus from published worked examples
5. Table data extracted to versioned data modules for the 60825-1 amendment cycle

## Help get this to v1.0

This package only becomes trustworthy if people other than its author check it, so contributions are genuinely wanted, especially from laser safety officers, photonics engineers and anyone who works with IEC 60825-1 day to day.

The most valuable things you can do:

- **Audit the outputs.** Run your own worked examples through it and compare against the standard, published references or a tool you already trust. A mismatch report is worth more than a feature.
- **Contribute test cases.** Reference values with a citation are what turn "seems right" into "verified", and they're what the golden corpus on the roadmap is made of.
- **Review the simplifications.** The table above lists every corner cut and which way the error runs. Tell us where a limit is wrong, or where a "conservative" label isn't earned.
- **Fill the gaps.** The pulsed engine, Condition 1 / 1M / 2M, skin MPEs and EN 207 mapping are all open.
- **Report anything surprising.** An output that doesn't match your expectation is a useful signal even if it turns out the package was right.

Open an issue or a pull request. See [CONTRIBUTING.md](CONTRIBUTING.md) for working from source, running the tests and the conventions the code follows.

## License

MIT. See [LICENSE](LICENSE).
