Repository: DCC-Lab/RayTracing

Issue:
Rays() accepts objects from every type
This can cause further problems. In `__init__` itself it is not a big deal (except for coherence and logic) if `Rays` receive a string of nonsense or a list of numbers, but other methods will inevitably fail with exceptions of different types.

Comments:

Patch:
diff --git a/raytracing/rays.py b/raytracing/rays.py
index 11de6ad..283eb00 100644
--- a/raytracing/rays.py
+++ b/raytracing/rays.py
@@ -4,20 +4,36 @@ import matplotlib.pyplot as plt
 import pickle
 import time
 import os
-
-""" A group of rays kept as a list, to be used as a starting
-point (i.e. an object) or as a cumulative detector (i.e. at an image
-or output plane) for ImagingPath, MatrixGroup or any tracing function.
-Subclasses can provide a computed ray for Monte Carlo simulation.
+import collections.abc as collections
+
+""" A source or a detector of rays
+
+We can obtain intensity distributions at from given plane by propagating
+many rays and collecting them at another plane. `Rays` is the base class
+that provides the essential mechanisms to obtain histograms on a list 
+of rays (created or collected). This list of rays is a property of the base
+class.  Subclasses are specific to a given ray distribution (Lambertian for 
+instance) and will create each ray on demand, then store them as they go
+in the rays list. 
+
+It is an iterable object, which means it can be used in an expression 
+like `for ray in rays:` which is convenient both when propagating rays 
+or when analysing the resulting rays that reached a plane in ImagingPath, 
+MatrixGroup or any tracing function. 
 """
 
-
 class Rays:
     def __init__(self, rays=None):
         if rays is None:
             self.rays = []
         else:
-            self.rays = rays
+            if isinstance(rays, collections.Iterable):
+                if all([isinstance(ray, Ray) for ray in rays]):
+                    self.rays = list(rays)
+                else:
+                    raise TypeError("'rays' elements must be of type Ray.")
+            else:
+                raise TypeError("'rays' must be iterable (i.e. a list or a tuple of Ray).")
 
         self.iteration = 0
         self.progressLog = 10000
@@ -160,6 +176,8 @@ class Rays:
         return self.rays[item]
 
     def append(self, ray):
+        if not isinstance(ray, Ray):
+            raise TypeError("'ray' must be a 'Ray' object.")
         if self.rays is not None:
             self.rays.append(ray)
 
@@ -214,21 +232,6 @@ class Rays:
     # https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm
     # and https://stackoverflow.com/questions/3122049/drawing-an-anti-aliased-line-with-thepython-imaging-library
 
-    # @property
-    # def intensityError(self):
-    #     return list(map(lambda x : sqrt(x), self.distribution))
-
-    # @property
-    # def normalizedIntensity(self):
-    #     maxValue = max(self.values)
-    #     return list(map(lambda x : x/maxValue, self.distribution))
-
-    # @property
-    # def normalizedIntensityError(self):
-    #     maxValue = max(self.distribution)
-    #     return list(map(lambda x : x/maxValue, self.error))
-
-
 class UniformRays(Rays):
     def __init__(self, yMax=1.0, yMin=None, thetaMax=pi / 2, thetaMin=None, M=100, N=100):
         self.yMax = yMax
@@ -256,8 +259,8 @@ class LambertianRays(Rays):
         if yMin is None:
             self.yMin = -yMax
 
-        self.thetaMax = -pi / 2
-        self.thetaMin = pi / 2
+        self.thetaMin = -pi / 2
+        self.thetaMax = pi / 2
         self.M = M
         self.N = N
         self.I = I
@@ -288,9 +291,9 @@ class RandomRays(Rays):
 
     def __getitem__(self, item):
         if self.rays is None:
-            raise NotImplemented("You cannot access RandomRays")
+            raise NotImplementedError("You cannot access RandomRays")
         elif len(self.rays) < item:
-            raise NotImplemented("You cannot access RandomRays")
+            raise NotImplementedError("You cannot access RandomRays")
         else:
             return self.rays[item]
 
@@ -301,7 +304,7 @@ class RandomRays(Rays):
         return self.randomRay()
 
     def randomRay(self) -> Ray:
-        raise NotImplemented("You must implement randomRay() in your subclass")
+        raise NotImplementedError("You must implement randomRay() in your subclass")
 
 
 class RandomUniformRays(RandomRays):


Test patch:
diff --git a/raytracing/tests/testsComponents.py b/raytracing/tests/testsComponents.py
new file mode 100644
index 0000000..a4aba5b
--- /dev/null
+++ b/raytracing/tests/testsComponents.py
@@ -0,0 +1,60 @@
+import unittest
+import envtest  # modifies path
+from raytracing import *
+
+inf = float("+inf")
+
+
+class Test4fSystem(unittest.TestCase):
+
+    def test4fSystem(self):
+        elements = [Space(10), Lens(10), Space(15), Lens(5), Space(5)]
+        mg = MatrixGroup(elements, label="4f system")
+        system = System4f(10, 5, label="4f system")
+        self.assertEqual(system.A, -0.5)
+        self.assertEqual(system.B, 0)
+        self.assertEqual(system.C, 0)
+        self.assertEqual(system.D, -2)
+        self.assertEqual(system.L, 30)
+        self.assertEqual(mg.backIndex, system.backIndex)
+        self.assertEqual(mg.frontIndex, system.frontIndex)
+        self.assertEqual(mg.backVertex, system.backVertex)
+        self.assertEqual(mg.frontVertex, system.frontVertex)
+        self.assertEqual(mg.label, system.label)
+        self.assertTrue(system.isImaging)
+
+    def test2fSystem(self):
+        elements = [Space(10), Lens(10), Space(10)]
+        mg = MatrixGroup(elements, label="2f system")
+        system = System2f(10, label="2f system")
+        self.assertEqual(system.A, 0)
+        self.assertEqual(system.B, 10)
+        self.assertEqual(system.C, -1 / 10)
+        self.assertEqual(system.D, 0)
+        self.assertEqual(system.L, 20)
+        self.assertEqual(mg.backIndex, system.backIndex)
+        self.assertEqual(mg.frontIndex, system.frontIndex)
+        self.assertEqual(mg.backVertex, system.backVertex)
+        self.assertEqual(mg.frontVertex, system.frontVertex)
+        self.assertEqual(mg.label, system.label)
+        self.assertFalse(system.isImaging)
+
+    def test4fIsTwo2f(self):
+        f1, f2 = 10, 12
+        system4f = System4f(f1=10, f2=12)
+        system2f_1 = System2f(f1)
+        system2f_2 = System2f(f2)
+        composed4fSystem = MatrixGroup(system2f_1.elements + system2f_2.elements)
+        self.assertEqual(composed4fSystem.A, system4f.A)
+        self.assertEqual(composed4fSystem.B, system4f.B)
+        self.assertEqual(composed4fSystem.C, system4f.C)
+        self.assertEqual(composed4fSystem.D, system4f.D)
+        self.assertEqual(composed4fSystem.L, system4f.L)
+        self.assertEqual(composed4fSystem.backIndex, system4f.backIndex)
+        self.assertEqual(composed4fSystem.frontIndex, system4f.frontIndex)
+        self.assertEqual(composed4fSystem.backVertex, system4f.backVertex)
+        self.assertEqual(composed4fSystem.frontVertex, system4f.frontVertex)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/raytracing/tests/testsRays.py b/raytracing/tests/testsRays.py
index 9bb4c7c..1638b29 100644
--- a/raytracing/tests/testsRays.py
+++ b/raytracing/tests/testsRays.py
@@ -7,6 +7,31 @@ inf = float("+inf")
 
 class TestRays(unittest.TestCase):
 
+    def testRaysInitDifferentInputs(self):
+        listOfRays = [Ray(), Ray(1, 1), Ray(1, -2), Ray(0, -1)]
+        tupleOfRays = tuple(listOfRays)
+        npArrayOfRays = array(listOfRays)
+        raysFromList = Rays(listOfRays)
+        raysFromTuple = Rays(tupleOfRays)
+        raysFromArray = Rays(npArrayOfRays)
+        rays = Rays(listOfRays)
+        self.assertListEqual(raysFromList.rays, listOfRays)
+        self.assertListEqual(raysFromTuple.rays, listOfRays)
+        self.assertListEqual(raysFromArray.rays, listOfRays)
+        self.assertListEqual(Rays(rays).rays, listOfRays)
+
+        with self.assertRaises(TypeError) as error:
+            # This should raise an TypeError exception
+            Rays("Ray(), Ray(1), Ray(1,1)")
+
+        with self.assertRaises(TypeError) as error:
+            # This should raise an TypeError exception
+            Rays([Ray(), [1, 2], Ray()])
+
+        with self.assertRaises(TypeError) as error:
+            # This should raise an TypeError exception
+            Rays(Matrix())
+
     def testRayCountHist(self):
         r = Rays([Ray()])
         init = r.rayCountHistogram()
diff --git a/raytracing/tests/testsRaysSubclasses.py b/raytracing/tests/testsRaysSubclasses.py
new file mode 100644
index 0000000..0b3710d
--- /dev/null
+++ b/raytracing/tests/testsRaysSubclasses.py
@@ -0,0 +1,14 @@
+import unittest
+import envtest  # modifies path
+from raytracing import *
+
+inf = float("+inf")
+
+
+class TestRandomRays(unittest.TestCase):
+
+    def testRandomRay(self):
+        rays = RandomRays()  # We keep default value, we are not intersted in the construction of a specific object
+        with self.assertRaises(NotImplementedError):
+            # This works
+            rays.randomRay()