Skip to content

fix(zemax): correct unit handling and folded-system geometry in importer#95

Open
beneaze wants to merge 3 commits into
mainfrom
fix/zemax-import-integration
Open

fix(zemax): correct unit handling and folded-system geometry in importer#95
beneaze wants to merge 3 commits into
mainfrom
fix/zemax-import-integration

Conversation

@beneaze

@beneaze beneaze commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

Several long-standing correctness issues in the Zemax -> component-editor import. The integration worked structurally but the semantics were off in several places. This PR reworks parsing and conversion, and adds a synthetic .zmx fixture so the previously-skipped parser/converter tests run on every PR.

Parser

  • UNIT card parsed. MM | CM | M | IN are normalised to mm at parse time: DISZ/DIAM/ENPD are multiplied by the scale; CURV is divided. Previously inch-unit files were silently treated as mm and imported 25.4x too small.
  • DIAM is a semi-diameter, not a full diameter. Field renamed diameter -> semi_diameter_mm; the converter no longer divides by two again. A 1 in achromat (DIAM 12.7) now imports as a 25.4 mm-tall component, not 12.7 mm.
  • PARM N value parsed. Length-valued PARMs on COORDBRK surfaces are UNIT-scaled. New is_mirror / is_coordinate_break / is_aspheric helpers on ZemaxSurface.

Converter

  • 2D frame (pos_x, pos_y, angle_rad) replaces the previous cumulative_x + direction scalar. Each interface line is placed perpendicular to the current propagation direction.
  • COORDBRK rotates / translates the frame: PARM 1 = decenter perpendicular to current axis, PARM 4 = in-plane tilt about y. Out-of-plane PARMs (2/3/5) cannot be represented in 2D and are dropped.
  • GLAS MIRROR emits element_type=mirror; angle_rad += pi after a planar reflection. Multi-mirror systems (for example, periscopes with two non-parallel folds) now unfold correctly.
  • EVENASPH / ODDASPHE are approximated as spheres at their base radius and annotated with (Asphere approx.). A warning is added to component notes + log service.
  • STOP annotation: a stop surface that survives the dummy filter gets (Aperture Stop) appended to its name.
  • Image surface (last) and flat air-to-air dummies are dropped: no more ghost interfaces cluttering the import.
  • COMM no longer overwrites the auto-generated name. It is prefixed instead, so part numbers and material/curvature info both survive.
  • Dead sag block removed: the renderer already derives sag from radius_of_curvature_mm.

Examples

examples/zemax_parse_simple.py is now a 75-line thin wrapper over the canonical ZemaxParser + ZemaxToInterfaceConverter. It previously had its own 263-line inline parser with the same DIAM-as-full-diameter bug, perpetuating the misconception.

Tests

  • tests/fixtures/sample.zmx: synthetic AC254-100-B-shaped prescription so the parser/converter tests run on every PR (they were universally skipped before because no fixture was checked in).
  • New coverage in test_zemax_import.py:
    • TestUnitParsing - mm/cm/in/unknown scaling, including end-to-end conversion of an inch-unit prescription
    • TestMirrorHandling - GLAS MIRROR -> mirror element, direction flip, curved mirror
    • TestSurfaceFiltering - image plane skipped, dummy air-to-air dropped, flat refractives kept
    • TestNamePreservation - COMM preserved alongside auto-generated descriptive name
    • TestCoordinateBreak - PARM parsing with UNIT scaling, in-plane tilt rotates next interface, perpendicular decenter shifts vertex
    • TestPeriscope - two 45 degree fold mirrors leave the optical axis parallel to entry but offset in y
    • TestAsphereAnnotation, TestStopAnnotation, TestMirrorFlag
  • test_zemax_converter.py: dropped stale interfaces_v2 / kind references that were preventing the fixture-driven tests from running.
  • UI-level smoke coverage now exercises the Component Editor Zemax import path with the checked-in .zmx fixture.

Test count: 15 active -> 56 active.

Test plan

  • pytest tests/services/test_zemax_converter.py tests/services/test_zemax_parser.py tests/services/test_zemax_import.py - 56 pass
  • python examples/zemax_parse_simple.py tests/fixtures/sample.zmx - prints the expected component summary (1 in achromat, 25.4 mm full aperture, 3 interfaces with right radii / material progression)
  • Adjacent service tests (test_paths_red, test_linked_assembly_service, test_storage_service) still pass
  • uv run --extra dev pytest tests/ui/test_component_editor.py::test_component_editor_imports_zemax_fixture -q
  • uv run --extra dev ruff check tests/ui/test_component_editor.py
  • Local real-file smoke: ZemaxImporter.import_file() with Thorlabs LA1131-A.zmx from the local vendor catalog imports LA1131-A N-BK7 Plano-Convex Lens as 2 interfaces with 50.8 mm aperture

Out of scope (deliberately)

  • Out-of-plane PARMs on COORDBRK (y-decenter, tilt-about-x, tilt-about-z): the editor is 2D.
  • Aspheric coefficients beyond the base radius.
  • Inline Tilt/Decenter group cards on STANDARD surfaces (legacy alternative to COORDBRK). Modern Zemax files use COORDBRK.

Generated with Claude Code and Codex.

beneaze and others added 3 commits May 11, 2026 20:03
Several long-standing correctness issues in the Zemax → component-editor
import. Reworks parsing and conversion; adds a synthetic .zmx fixture so
the previously-skipped parser/converter tests run on every PR.

Parser:
- UNIT MM | CM | M | IN parsed; lengths normalised to mm at parse time
  (DISZ/DIAM/ENPD multiplied, CURV divided).
- DIAM renamed semi_diameter_mm to reflect that the Zemax card stores a
  semi-diameter, not a full diameter. The converter no longer divides by
  two again — imported lenses now render at the right physical size.
- PARM N value cards parsed; length-valued PARMs on COORDBRK surfaces are
  UNIT-scaled. Adds is_mirror / is_coordinate_break / is_aspheric helpers.

Converter:
- Tracks a 2D frame (pos_x, pos_y, angle_rad) and places each interface
  perpendicular to the current propagation direction. COORDBRK PARMs 1
  (perpendicular decenter) and 4 (in-plane tilt) update the frame.
- GLAS MIRROR emits element_type=mirror; angle flips by pi after a planar
  reflection. Multi-mirror systems (e.g. periscopes) now unfold correctly
  in the 2D editor.
- EVENASPH / ODDASPHE surfaces are approximated at their base radius and
  annotated; aspheric coefficients are not represented.
- STOP flag surviving the dummy filter is annotated as "Aperture Stop".
- Image surface (last) and flat air-to-air dummies are dropped.
- COMM no longer overwrites the auto-generated material/curvature name.
- Dead surface-sag block removed (renderer derives sag from radius).

Examples:
- examples/zemax_parse_simple.py replaces a 263-line copy-pasted parser
  (which still had the original DIAM-as-full-diameter bug) with a thin
  wrapper over the canonical ZemaxParser + ZemaxToInterfaceConverter.

Tests:
- tests/fixtures/sample.zmx: synthetic AC254-100-B prescription un-skips
  the parser/converter tests (was skipped on every run before).
- New coverage in test_zemax_import.py: UNIT scaling (mm/cm/in/unknown),
  mirror mapping + direction flip, dummy/stop filtering, image-surface
  skip, COMM preservation, COORDBRK decenter + tilt, two-mirror periscope
  end-to-end, asphere annotation, stop annotation.
- test_zemax_converter.py: dropped stale interfaces_v2/kind references.

Test count: 15 active → 56 active (47 new + reactivated coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the parser/converter changes under Added (UNIT, COORDBRK, mirror
mapping, asphere/stop annotations), Changed (COMM preservation, examples
wrapper), and Fixed (DIAM semi-diameter, ghost interfaces, test coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants