From 41bc61c7653c6095a681d4f44f8a9ce05b115f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Tue, 3 Mar 2026 21:49:16 -0500 Subject: [PATCH] Add Conjugation enum and conjugation properties to Matrix Classify all four ABCD matrix conjugation types (B=0, A=0, D=0, C=0) with a new Conjugation enum and convenience boolean properties (isAfocal, isInfiniteFiniteConjugate, isFiniteInfiniteConjugate). Co-Authored-By: Claude Opus 4.6 --- raytracing/matrix.py | 94 +++++++++++++++++++++++++++++ raytracing/tests/testsComponents.py | 5 ++ raytracing/tests/testsMatrix.py | 73 ++++++++++++++++++++++ 3 files changed, 172 insertions(+) diff --git a/raytracing/matrix.py b/raytracing/matrix.py index 81cfe88b..6cfb31d8 100644 --- a/raytracing/matrix.py +++ b/raytracing/matrix.py @@ -5,6 +5,7 @@ from .interface import * from .utils import * +from enum import Enum from typing import List import multiprocessing import sys @@ -35,6 +36,25 @@ class Conjugate(NamedTuple): d: float = None transferMatrix:'Matrix' = None + +class Conjugation(Enum): + """The four conjugation types of an ABCD transfer matrix, + each corresponding to one matrix element being zero. + + y₂ = A·y₁ + B·θ₁ + θ₂ = C·y₁ + D·θ₁ + + FiniteFinite (B=0): y₂ depends only on y₁ + InfiniteFinite (A=0): y₂ depends only on θ₁ + FiniteInfinite (D=0): θ₂ depends only on y₁ + Afocal (C=0): θ₂ depends only on θ₁ + """ + FiniteFinite = "FiniteFinite" + InfiniteFinite = "InfiniteFinite" + FiniteInfinite = "FiniteInfinite" + Afocal = "Afocal" + + # todo: fix docstrings since draw-related methods were removed @@ -1081,10 +1101,84 @@ def isImaging(self): A = transverse magnification D = angular magnification And as usual, C = -1/f (always). + + See Also + -------- + conjugation : Returns the full Conjugation enum type (FiniteFinite, + InfiniteFinite, FiniteInfinite, or Afocal). """ return isAlmostZero(self.B, self.__epsilon__) + @property + def conjugation(self): + """Returns the conjugation type of this matrix as a Conjugation enum, + or None if no ABCD element is zero. + + The four types correspond to one matrix element being zero: + - FiniteFinite (B=0): object and image are at finite distances + - InfiniteFinite (A=0): object at infinity, image at finite distance + - FiniteInfinite (D=0): object at finite distance, image at infinity + - Afocal (C=0): both object and image at infinity + + Priority order when multiple elements are zero: B, A, D, C. + + Examples + -------- + >>> from raytracing import * + >>> m = Space(10) * Lens(5) * Space(10) + >>> m.conjugation + + """ + if isAlmostZero(self.B, self.__epsilon__): + return Conjugation.FiniteFinite + elif isAlmostZero(self.A, self.__epsilon__): + return Conjugation.InfiniteFinite + elif isAlmostZero(self.D, self.__epsilon__): + return Conjugation.FiniteInfinite + elif isAlmostZero(self.C, self.__epsilon__): + return Conjugation.Afocal + return None + + @property + def isAfocal(self): + """True if C=0 (no optical power), meaning the system is afocal + (e.g. a telescope). This is the complement of hasPower. + + Examples + -------- + >>> from raytracing import * + >>> System4f(10, 5).isAfocal + True + """ + return isAlmostZero(self.C, self.__epsilon__) + + @property + def isInfiniteFiniteConjugate(self): + """True if A=0, meaning an object at infinity is imaged at a + finite distance (e.g. a single lens with object at infinity). + + Examples + -------- + >>> from raytracing import * + >>> System2f(10).isInfiniteFiniteConjugate + True + """ + return isAlmostZero(self.A, self.__epsilon__) + + @property + def isFiniteInfiniteConjugate(self): + """True if D=0, meaning an object at a finite distance produces + a collimated output (e.g. object at focal point of a lens). + + Examples + -------- + >>> from raytracing import * + >>> (Lens(10) * Space(10)).isFiniteInfiniteConjugate + True + """ + return isAlmostZero(self.D, self.__epsilon__) + @property def hasPower(self): """ If True, then there is a non-null focal length because C!=0. We compare to an epsilon value, because diff --git a/raytracing/tests/testsComponents.py b/raytracing/tests/testsComponents.py index bb4868b8..d87949e0 100644 --- a/raytracing/tests/testsComponents.py +++ b/raytracing/tests/testsComponents.py @@ -55,5 +55,10 @@ def test4fIsTwo2f(self): self.assertEqual(composed4fSystem.frontVertex, system4f.frontVertex) + def testTelescopeIsAfocal(self): + system = System4f(10, 5) + self.assertTrue(system.isAfocal) + + if __name__ == '__main__': envtest.main() diff --git a/raytracing/tests/testsMatrix.py b/raytracing/tests/testsMatrix.py index 4fd9a227..6b6216b7 100644 --- a/raytracing/tests/testsMatrix.py +++ b/raytracing/tests/testsMatrix.py @@ -1160,5 +1160,78 @@ def testTraceManyOpenCLBlockedRay(self): self.assertTrue(traces[1][-1].isBlocked) +class TestConjugation(envtest.RaytracingTestCase): + + def testConjugationFiniteFiniteBZero(self): + m = Space(10) * Lens(5) * Space(10) + self.assertEqual(m.conjugation, Conjugation.FiniteFinite) + + def testConjugationInfiniteFiniteAZero(self): + m = System2f(10) + self.assertEqual(m.conjugation, Conjugation.InfiniteFinite) + + def testConjugationFiniteInfiniteDZero(self): + m = Lens(10) * Space(10) + self.assertEqual(m.conjugation, Conjugation.FiniteInfinite) + + def testConjugationAfocalCZero(self): + m = Space(10) # free space: A=1, B=10, C=0, D=1 + self.assertEqual(m.conjugation, Conjugation.Afocal) + + def testConjugationNoneWhenNoElementZero(self): + m = Matrix(2, 1, 1, 1) # det = 2*1 - 1*1 = 1, no element is zero + self.assertIsNone(m.conjugation) + + def testConjugationPriorityIdentityMatrix(self): + m = Matrix(1, 0, 0, 1) + self.assertEqual(m.conjugation, Conjugation.FiniteFinite) + + def testConjugationConsistencyWithIsImaging(self): + m = Space(10) * Lens(5) * Space(10) + self.assertEqual(m.conjugation == Conjugation.FiniteFinite, m.isImaging) + + def testConjugationConsistencyWithIsImagingFalse(self): + m = Matrix(2, 1, 1, 1) # det=1, no B=0 + self.assertEqual(m.conjugation == Conjugation.FiniteFinite, m.isImaging) + + def testIsAfocalTrue(self): + m = System4f(10, 5) + self.assertTrue(m.isAfocal) + + def testIsAfocalFalse(self): + m = Lens(10) + self.assertFalse(m.isAfocal) + + def testIsAfocalConsistencyWithHasPower(self): + m = System4f(10, 5) + self.assertEqual(m.isAfocal, not m.hasPower) + + def testIsAfocalConsistencyWithHasPowerLens(self): + m = Lens(10) + self.assertEqual(m.isAfocal, not m.hasPower) + + def testIsInfiniteFiniteConjugateTrue(self): + m = System2f(10) + self.assertTrue(m.isInfiniteFiniteConjugate) + + def testIsInfiniteFiniteConjugateFalse(self): + m = Matrix(1, 2, 0, 1) + self.assertFalse(m.isInfiniteFiniteConjugate) + + def testIsFiniteInfiniteConjugateTrue(self): + m = Lens(10) * Space(10) + self.assertTrue(m.isFiniteInfiniteConjugate) + + def testIsFiniteInfiniteConjugateFalse(self): + m = Matrix(1, 0, 0, 1) + self.assertFalse(m.isFiniteInfiniteConjugate) + + def testConjugationLensAt2f(self): + f = 10 + m = Space(2 * f) * Lens(f) * Space(2 * f) + self.assertEqual(m.conjugation, Conjugation.FiniteFinite) + self.assertTrue(m.isImaging) + + if __name__ == '__main__': envtest.main()