An extension to pyldt for symmetrising EULUMDAT (.ldt) photometric files.
Developed in an ISO 17025 accredited photometry laboratory.
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.
pip install eulumdat-symmetryRequires eulumdat-py >= 1.0.0 (pyldt).
| 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 |
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)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.")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.23from 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")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.
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).
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.
eulumdat-symmetry/
├── ldt_symmetry/
│ ├── __init__.py ← Public API (LdtSymmetriser, LdtAutoDetector)
│ ├── symmetriser.py ← LdtSymmetriser
│ ├── auto_detector.py ← LdtAutoDetector
│ └── _geometry.py ← Internal geometry and scoring functions
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) |
New to the ecosystem? eulumdat-quickstart — a step-by-step guide covering all 8 packages with working examples.
| Package | Description |
|---|---|
| eulumdat-py | Read / write EULUMDAT files |
eulumdat-symmetry |
Symmetrise and detect ISYM — this package |
| eulumdat-plot | Polar intensity diagram (SVG/PNG) |
| eulumdat-luminance | Luminance table and polar diagram |
| eulumdat-ugr | UGR catalogue (CIE 117/190) |
| eulumdat-analysis | Beam half-angle, FWHM |
| eulumdat-report | Full photometric datasheet (HTML/PDF) |
| eulumdat-ies | LDT ↔ IES LM-63-2002 conversion |
github.com/123VincentB/eulumdat-symmetry
MIT