eulumdat-symmetry
An extension to pyldt for symmetrising EULUMDAT (.ldt) photometric files.
Developed in an ISO 17025 accredited photometry laboratory.
Overview
EULUMDAT files store luminous intensity distributions as a [mc × ng] matrix of C-planes and γ-angles. When a luminaire has a symmetric distribution, the file can declare this via the ISYM field (1–4), which allows lighting design software to reconstruct the full distribution from a subset of C-planes.
This library provides two tools:
LdtSymmetriser— symmetrise apyldt.Ldtobject for a known ISYM mode. Deterministic, no configuration needed.LdtAutoDetector— automatically detect the most plausible ISYM mode from the shape of the distribution. Optional; use only when the mode is not known in advance.
Installation
pip install eulumdat-symmetry
Requires eulumdat-py >= 0.1.4 (pyldt).
ISYM codes
| ISYM | Description | C-planes symmetrised |
|---|---|---|
| 0 | No symmetry | — |
| 1 | Full rotational symmetry | All planes averaged to a single profile |
| 2 | Symmetry about C0–C180 | C and 360°−C averaged |
| 3 | Symmetry about C90–C270 | C and 180°−C averaged |
| 4 | Quadrant symmetry | All 4 quadrant mirrors averaged |
Usage
Known symmetry mode
from pyldt import LdtReader, LdtWriter
from ldt_symmetry import LdtSymmetriser
ldt = LdtReader.read("luminaire.ldt")
ldt_sym = LdtSymmetriser.symmetrise(ldt, isym=2)
LdtWriter.write(ldt_sym, "luminaire_SYM.ldt")
LdtSymmetriser.symmetrise() returns a new Ldt object — the original is never modified.
If the file already declares a non-zero ISYM, the function returns an unchanged copy by default (the distribution has already been symmetrised at measurement time). Use force=True to re-symmetrise regardless.
ldt_sym = LdtSymmetriser.symmetrise(ldt, isym=2, force=True)
Automatic detection
from pyldt import LdtReader, LdtWriter
from ldt_symmetry import LdtAutoDetector, LdtSymmetriser
ldt = LdtReader.read("luminaire.ldt")
detector = LdtAutoDetector()
isym = detector.detect(ldt)
if isym != 0:
ldt_sym = LdtSymmetriser.symmetrise(ldt, isym=isym)
LdtWriter.write(ldt_sym, "luminaire_SYM.ldt")
else:
print("No plausible symmetry detected — file left unchanged.")
Automatic detection with diagnostic
When return_diag=True, detect() returns a (isym, diag) tuple. The diag dict contains the shape metrics, symmetry scores, and the veto decision — useful for understanding why a particular mode was accepted or rejected.
isym, diag = detector.detect(ldt, return_diag=True)
print(f"Detected ISYM: {isym}")
print(f"Candidate ISYM (shape stage): {diag['isym_candidate']}")
print(f"Accepted: {diag['accepted']}")
print(f"Scores: {diag['scores']}")
# diag['scores'] = {1: 0.12, 2: 0.08, 3: 0.31, 4: 0.31}
# diag['score_threshold'] = 0.23
Batch processing
from pathlib import Path
from pyldt import LdtReader, LdtWriter
from ldt_symmetry import LdtAutoDetector, LdtSymmetriser
detector = LdtAutoDetector()
for path in Path("ldt_files").glob("*.ldt"):
ldt = LdtReader.read(path)
isym = detector.detect(ldt)
if isym != 0:
ldt_sym = LdtSymmetriser.symmetrise(ldt, isym=isym)
out = path.with_stem(path.stem + "_SYM")
LdtWriter.write(ldt_sym, out)
print(f"{path.name} → ISYM {isym} → {out.name}")
else:
print(f"{path.name} → no symmetry detected")
How automatic detection works
Detection runs in two stages:
Stage 1 — Shape analysis
The intensity distribution is projected onto the floor (γ < 90°) and ceiling (γ > 90°) planes using the point-source illuminance formula E = I·cos³(γ)/d². Weighted moments (centroid offset, ellipse anisotropy, principal axis orientation) and angular harmonics (H4 for quadrant detection) are computed for each hemisphere. A candidate mode is chosen by combining the decisions from both hemispheres.
Stage 2 — Score veto
The relative RMS asymmetry is measured for the candidate mode. If the score exceeds the threshold, the candidate is rejected and ISYM 0 is returned. This prevents the shape stage from proposing a symmetric mode when the distribution is actually irregular.
Both stages must agree for a mode to be accepted.
Validation
The detection algorithm was validated on a dataset of 42 real manufacturer LDT files covering all five ISYM modes (0–4), selected for their representativeness from several hundred laboratory measurements. No misclassification was observed (42/42 correct).
Adjusting detection thresholds
The default thresholds work well for typical manufacturer files. If you observe incorrect decisions on your dataset, you can adjust them at instantiation:
detector = LdtAutoDetector(
score_th=0.20, # stricter veto for ISYM 2/3/4 (default: 0.23)
rot_score_th=0.15, # stricter veto for ISYM 1 (default: 0.20)
)
See the LdtAutoDetector docstring for the full list of parameters.
Project structure
ldt_symmetry/
├── __init__.py ← Public API (LdtSymmetriser, LdtAutoDetector)
├── symmetriser.py ← LdtSymmetriser
├── auto_detector.py ← LdtAutoDetector
└── _geometry.py ← Internal geometry and scoring functions
Relation to pyldt
eulumdat-symmetry is an extension to pyldt, not a replacement. It operates on pyldt.Ldt objects and delegates all file I/O to pyldt.LdtReader and pyldt.LdtWriter.
| Task | Tool |
|---|---|
Read / write .ldt files |
pyldt (LdtReader, LdtWriter) |
| Edit header fields | pyldt (LdtHeader) |
| Symmetrise the intensity distribution | eulumdat-symmetry (LdtSymmetriser) |
| Detect the symmetry mode automatically | eulumdat-symmetry (LdtAutoDetector) |
Source code
github.com/123VincentB/ldt_symmetry
License
MIT
Metadata
Release files for eulumdat-symmetry 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| eulumdat_symmetry-1.0.0.tar.gz | 20.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| eulumdat_symmetry-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.7 kB
Release files / eulumdat_symmetry-1.0.0.tar.gz
| Download URL | eulumdat_symmetry-1.0.0.tar.gz |
|---|---|
| Size | 20.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1e81c37dbd32eea4c5df6c642f5461ef51e2bcfa2d2b569aca8c71b85f42e0c2
|
|
BLAKE2b-256 checksum How to use checksums |
bda69d9fb0efc36459496813e82c32f6568235ad8322782d82936706fc58070a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.11
|
Release files / eulumdat_symmetry-1.0.0-py3-none-any.whl
| Download URL | eulumdat_symmetry-1.0.0-py3-none-any.whl |
|---|---|
| Size | 18.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
db6b4e69ccacccbbbdfd546f2b43c7f7b99ba6414d8697dd2b373fe5b8500e1c
|
|
BLAKE2b-256 checksum How to use checksums |
f127531d9c59f8a5cca6812a79866728ad21470b4c5a07852c410f71a9e91c29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.11
|