├── .gitattributes ├── .github └── workflows │ ├── pythonpublish.yml │ └── stale.yml ├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml ├── other.xml └── vcs.xml ├── .pylintrc ├── .readthedocs.yml ├── FAQ.md ├── LICENSE ├── MANIFEST.in ├── README-Adding-new-materials.md ├── README.assets ├── chromaticaberrations.png ├── e0-0356707.png ├── e0.png ├── ex01.png ├── ex02.png ├── ex03.png ├── ex04.png ├── ex05.png ├── ex06.png ├── ex07.png ├── ex08.png ├── ex09.png ├── ex10.png ├── ex11.png ├── ex12.png ├── ex14.png ├── ex15.png ├── ex16.1.png ├── ex16.2.png ├── ex16.3.png ├── ex17.png ├── ex18.png ├── hierarchy.png ├── image-20190511230415520.png ├── inputProfile.png ├── kholerIllumination.png ├── microscopeSimple.png ├── outputProfile.png ├── simple.png ├── simpleObjectImage.png ├── thorlabs-0356707.png └── thorlabs.png ├── README.md ├── docs ├── FAQ.rst ├── Makefile ├── README-documentation.md ├── _templates │ ├── autoClass.rst │ ├── autoFunction.rst │ ├── autoInheritedFunction.rst │ ├── classDefault.rst │ └── moduleDefault.rst ├── conf.py ├── contribute.md ├── examples.md ├── gettingStarted.md ├── images │ ├── AngleHist.png │ ├── Histogram.png │ ├── ImagingPath.png │ ├── LambertianRays.png │ ├── RandomLambertianRays.png │ ├── RandomUniformRays.png │ ├── UniformRays.png │ ├── apertureStop.png │ ├── displayRays.png │ ├── flipOrientation_after.png │ ├── flipOrientation_before.png │ └── matrixGraphicDisplay.png ├── index.rst ├── make.bat ├── raytracing.rst ├── reference.rst ├── requirements.txt └── webgraphviz_url │ ├── index.html │ ├── jquery.min.js │ ├── site.js │ └── viz.js ├── examples └── README.md ├── pyproject.toml └── raytracing ├── __init__.py ├── __main__.py ├── axicon.py ├── components.py ├── eo.py ├── examples ├── README.md ├── __init__.py ├── confocalDetection.py ├── envexamples.py ├── ex01.py ├── ex02.py ├── ex03.py ├── ex04.py ├── ex05.py ├── ex06.py ├── ex07.py ├── ex08.py ├── ex09.py ├── ex10.py ├── ex11.py ├── ex12.py ├── ex13.py ├── ex14.py ├── ex15.py ├── ex16.py ├── ex17.py ├── ex18.py ├── ex19.py ├── fig1-matrices.png ├── fig2-properties.png ├── fig3-apertures.png ├── fig4-overviewFigure.png ├── fig5-LSM_reference.png ├── fig6-lsmConfocalpinhole.py ├── fig7-kohler.py ├── fig8-widefield.py ├── illuminator.py ├── invariant.py ├── invariantAndEfficiency.py ├── kohlerAndInvariant.py ├── laserScanningMicroscope.py ├── multiProcessingTracing.py ├── parallel_time_smallcounts.pdf ├── parallel_time_smallcounts.png ├── randomLambertianRays.py ├── randomUniformRays.py ├── sourceCollection.py ├── template.py └── twoPhotonDescannedDetector.py ├── figure.py ├── gaussianbeam.py ├── graphicComponents.py ├── graphics.py ├── imagingpath.py ├── interface.py ├── lasercavity.py ├── laserpath.py ├── lensViewer.py ├── materials.py ├── matrix.py ├── matrixgroup.py ├── nikon.py ├── olympus.py ├── preferences.py ├── ray.py ├── rays.py ├── specialtylenses.py ├── specifications ├── AC254-100-A-Zemax(ZMX).zmx ├── AC254-125-A-Zemax(ZMX).zmx ├── ASL5040-UV-Zemax(ZMX).zmx ├── envtest.py ├── lensBuilder.py ├── olympus-table3.csv ├── references.md ├── thorlabs.Achromatic_Doublet_AR_Coatings.xlsx └── zmax_49270.zmx ├── tests ├── .coveragerc ├── envtest.py ├── parallel_time_smallcounts.png ├── shifts-ac254-100-A.txt ├── shifts-ac254-200-A.txt ├── testDisplay.py ├── testsAxicon.py ├── testsCallOtherScript.py ├── testsComponents.py ├── testsDecorators.py ├── testsDocTestModule.py ├── testsEnvtest.py ├── testsExamples.py ├── testsGaussian.py ├── testsImagingPath.py ├── testsLagrangeInvariantIdeas.py ├── testsLaserPath.py ├── testsLimits.py ├── testsMaterials.py ├── testsMatrix.py ├── testsMatrixGroup.py ├── testsMatrixSubclasses.py ├── testsMultiprocessor.py ├── testsPreferences.py ├── testsPreferencesRaytracing.py ├── testsRay.py ├── testsRays.py ├── testsRaysSubclasses.py ├── testsReport.py ├── testsSpecialLenses.py ├── testsSpecialtyLenses.py ├── testsTransforms.py ├── testsUtils.py ├── testsWarnings.py ├── testsZemax.py ├── traceManyThroughInParallelNoOutput.py └── traceManyThroughInParallelWithOutput.py ├── thorlabs.py ├── ui ├── __init__.py └── raytracing_app.py ├── utils.py └── zemax.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.6' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '37 11 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v3 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'Stale issue message' 25 | stale-pr-message: 'Stale pull request message' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | coverage_html_report/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | docs/modules/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv*/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | .dmypy.json 113 | dmypy.json 114 | 115 | # Pyre type checker 116 | .pyre/ 117 | test.py 118 | 119 | # Everything below is specific to JetBrains and PyCharm 120 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 121 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 122 | 123 | # User-specific stuff 124 | .idea/**/workspace.xml 125 | .idea/**/tasks.xml 126 | .idea/**/usage.statistics.xml 127 | .idea/**/dictionaries 128 | .idea/**/shelf 129 | 130 | # Generated files 131 | .idea/**/contentModel.xml 132 | 133 | # Sensitive or high-churn files 134 | .idea/**/dataSources/ 135 | .idea/**/dataSources.ids 136 | .idea/**/dataSources.local.xml 137 | .idea/**/sqlDataSources.xml 138 | .idea/**/dynamic.xml 139 | .idea/**/uiDesigner.xml 140 | .idea/**/dbnavigator.xml 141 | 142 | # Gradle 143 | .idea/**/gradle.xml 144 | .idea/**/libraries 145 | 146 | # Gradle and Maven with auto-import 147 | # When using Gradle or Maven with auto-import, you should exclude module files, 148 | # since they will be recreated, and may cause churn. Uncomment if using 149 | # auto-import. 150 | # .idea/artifacts 151 | # .idea/compiler.xml 152 | # .idea/jarRepositories.xml 153 | # .idea/modules.xml 154 | # .idea/*.iml 155 | # .idea/modules 156 | # *.iml 157 | # *.ipr 158 | 159 | # CMake 160 | cmake-build-*/ 161 | 162 | # Mongo Explorer plugin 163 | .idea/**/mongoSettings.xml 164 | 165 | # File-based project format 166 | *.iws 167 | 168 | # IntelliJ 169 | out/ 170 | 171 | # mpeltonen/sbt-idea plugin 172 | .idea_modules/ 173 | 174 | # JIRA plugin 175 | atlassian-ide-plugin.xml 176 | 177 | # Cursive Clojure plugin 178 | .idea/replstate.xml 179 | 180 | # Crashlytics plugin (for Android Studio and IntelliJ) 181 | com_crashlytics_export_strings.xml 182 | crashlytics.properties 183 | crashlytics-build.properties 184 | fabric.properties 185 | 186 | # Editor-based Rest Client 187 | .idea/httpRequests 188 | 189 | # Android studio 3.1+ serialized cache file 190 | .idea/caches/build_file_checksums.ser 191 | 192 | .idea/RayTracing.iml 193 | 194 | .idea/misc.xml 195 | .DS_Store 196 | raytracing/_version.py 197 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF 17 | formats: [] 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.8 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: setuptools 25 | path: . 26 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | 4 | 5 | 1. **What is the formalism used and where can I learn about it?** 6 | 7 | We use the ray matrix formalism and you can learn about it in most optics books as well as in the publication: 8 | 9 | > ["Tools and tutorial on practical ray tracing for microscopy"](https://doi.org/10.1117/1.NPh.8.1.010801) 10 | > 11 | > by V. Pineau Noël\*, S. Masoumi\*, E. Parham*, G. Genest, L. Bégin, M.-A. Vigneault, D. C. Côté, 12 | > Neurophotonics, 8(1), 010801 (2021). 13 | > *Equal contributions. 14 | > Permalink: https://doi.org/10.1117/1.NPh.8.1.010801 15 | 16 | If you happen to speak French, D.C. Côté's Optique notes are available [online](https://books.apple.com/ca/book/optique/id949326768) and as PDF and there is a chapter on it. 17 | 18 | 2. **Can you do spherical aberrations?** 19 | 20 | No we cannot. It may appear at first sight that it is possible (we could easily correct the sinθ ≅ θ approximation when off-axis) but the ray matrix formalism does not allow us to consider the different propagation distances of the off-axis rays. By definition, for exemple, a element of `Space` has a given length `d` and it is the same for all rays. When considering spherical aberrations, this varying distance for rays that are off-axis is part of the aberration, which is not given solely by the correction to the angle. Since we cannot have a different distance for all rays (i.e. it is a matrix multiplication), then spherical aberrations are not possible (as far as we know). ***You think we are wrong? Teach us how to do it!*** 21 | 22 | 3. **Can you do chromatic aberrations?** 23 | 24 | Yes we can, when the lenses are built from dielectric interfaces and materials with `Materials`. The Thorlabs and Edmund Optics lenses are built with a radius of curvature and the proper material as obtained from the excellent site http://refractiveindex.info. 25 | It is not possible however to consider chromatic aberrations for a thin lens. The thin lens is created directly with a given focal length and no material at all (it is infinitely thin). The focal length is fixed. To create a lens with chromatic aberration, you need to use `SingletLens` or `AchromaticDoubletLens`. 26 | 27 | 4. **Can I perform more complex calculations than just tracing rays?** 28 | Of course you can. Although the display functions are central to the module, you can also extract more information in a script. For instance, the example [lsmConfocalPinhole.py](https://github.com/DCC-Lab/RayTracing/blob/master/raytracing/examples/fig6-lsmConfocalpinhole.py) is a good example where the transmission efficacy of a pinhole is calculated as a function of distance from the focal plane. 29 | 30 | 5. **Can you perform a 2D efficiency calculation that will apply to my real-life optical system?** 31 | Yes, but you need to use a ray distribution that considers the cylindrical symmetry (currently in preparation). This will provide proper numbers for the 2D energy efficiency, but it will not be "traceable" easily. To trace your rays, use the `RandomUniformRays` class for instance. 32 | 33 | 6. **There are stored preferences. Where are they?** 34 | Some variables can be "saved" into a Preferences file on disk. Where it is depends on the platform. The file is always the same (it is a JSON dictionary) but the location is platform-specific: 35 | 36 | * macOS: `(username)/Library/Preferences/ca.dcclab.python.raytracing.json` 37 | 38 | * Windows: `(username)/AppData(something-something)/ca.dcclab.python.raytracing.json` 39 | 40 | * linux: `(username)/.config/ca.dcclab.python.raytracing.json` 41 | 42 | 43 | Some of the variables that are stored are: `lastVersionCheck` (date of the last version check on PyPI) and `mode` which can be `beginner`, `expert`, or `silent`. 44 | 45 | 7. **There are a lot of warnings printed on screen, and I don't like it because I know what I am doing. Can I get rid of that?** 46 | Yes you can. There are three modes: `beginner` (many warnings), `expert` (very few, but some), and `silent` (nothing). Call `silentMode(saveToPrefs=True)` or `expertMode(saveToPrefs=True)` and you will be all set. 47 | 48 | 8. **What is the front and the back of a lens?** 49 | A lens has a focus on both sides. The side where the light enters is called the front side, and therefore the focus on that side is at a distance "front focal length (or FFL)" away from the physical surface of the lens. Similarly (and possibly surprisingly) the focus to which the light will go after having propagated through the lens is the "back focus" and is a distance "back focal length or (BFL)" away from the last physical surface of the lens. 50 | 51 | These terms are standard terms in optical design. 52 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 DCCLab and Daniel C. Côté 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include assets/* 3 | include examples/* 4 | include doc/*.html -------------------------------------------------------------------------------- /README-Adding-new-materials.md: -------------------------------------------------------------------------------- 1 | ## Adding a new material to the Raytracing module 2 | 3 | If you attempt to read a Zemax file with ZMXReader, you may encounter an error if the material is not recognized: 4 | 5 | ```shell 6 | ValueError: The requested material 'SomeWeirdMaterial' is not recognized in the list of materials of raytracing: ['Air', 'N_BK7', 'N_SF2', 'N_SF8', 'SF2', 'SF5', 'N_SF5', 'N_SF6', 'N_SF6HT', 'N_SF10', 'N_SF11', 'N_SF57', 'N_BAF10', 'E_BAF11', 'N_BAK1', 'N_BAK4', 'FK51A', 'LAFN7', 'N_LASF9', 'N_LAK22', 'N_SSK5', 'E_FD10', 'FusedSilica']. You need to implement it as asubclass of Material, see materials.py for examples. 7 | ``` 8 | 9 | 10 | 11 | The reason is that there are tons of materials available and it is ridiculous to enter them all ifthey are not used in optical components. With the excellent http://refractiveindex.info, we are always just one click away from an answer. 12 | 13 | ## How do you add a new material? 14 | 15 | It is in fact quite simple: 16 | 17 | 1. Take a look at `materials.py` for many examples. 18 | 19 | 2. Derive a class from material, give it the name of your material (e.g., `N_LAK21`) 20 | 21 | 3. Then you need to define `n()` and the Abbe number: 22 | 23 | ```python 24 | @classmethod 25 | def n(cls, wavelength): # Wavelengths are always in microns (like Zemax, refractiveindex.info, etc). 26 | return calculate the value 27 | 28 | @classmethod 29 | def abbeNumber(cls): 30 | return aValue 31 | ``` 32 | 33 | 4. All you need to define is in this example for N_BK7: 34 | 35 | ```python 36 | class N_BK7(Material): 37 | """ All data from https://refractiveindex.info/tmp/data/glass/schott/N-BK7.html """ 38 | @classmethod 39 | def n(cls, wavelength): 40 | if wavelength > 10 or wavelength < 0.01: 41 | raise ValueError("Wavelength must be in microns") 42 | x = wavelength 43 | 44 | n=(1+1.03961212/(1-0.00600069867/x**2)+0.231792344/(1-0.0200179144/x**2)+1.01046945/(1-103.560653/x**2))**.5 45 | return n 46 | 47 | @classmethod 48 | def abbeNumber(cls): 49 | return 64.17 50 | ``` 51 | 52 | ## Will Raytracing know about my new material? 53 | 54 | Yes it will, through the magic of Python, it is possible to know when subclasses are created. 55 | 56 | There are functions to find materials (such as `Material.findByName()` or `Material.findByIndex()`. You actually don't need to include your code into the main raytracing code: if you derive a class from Material (as above) it will be included in the search without having to do anything. 57 | 58 | You can then send it to us, we'll include it if it is relevant. Send to dccote@cervo.ulaval.ca, or create an [Issue on GitHub](https://github.com/DCC-Lab/RayTracing/issues) and paste your code. 59 | 60 | -------------------------------------------------------------------------------- /README.assets/chromaticaberrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/chromaticaberrations.png -------------------------------------------------------------------------------- /README.assets/e0-0356707.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/e0-0356707.png -------------------------------------------------------------------------------- /README.assets/e0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/e0.png -------------------------------------------------------------------------------- /README.assets/ex01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex01.png -------------------------------------------------------------------------------- /README.assets/ex02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex02.png -------------------------------------------------------------------------------- /README.assets/ex03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex03.png -------------------------------------------------------------------------------- /README.assets/ex04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex04.png -------------------------------------------------------------------------------- /README.assets/ex05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex05.png -------------------------------------------------------------------------------- /README.assets/ex06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex06.png -------------------------------------------------------------------------------- /README.assets/ex07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex07.png -------------------------------------------------------------------------------- /README.assets/ex08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex08.png -------------------------------------------------------------------------------- /README.assets/ex09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex09.png -------------------------------------------------------------------------------- /README.assets/ex10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex10.png -------------------------------------------------------------------------------- /README.assets/ex11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex11.png -------------------------------------------------------------------------------- /README.assets/ex12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex12.png -------------------------------------------------------------------------------- /README.assets/ex14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex14.png -------------------------------------------------------------------------------- /README.assets/ex15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex15.png -------------------------------------------------------------------------------- /README.assets/ex16.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex16.1.png -------------------------------------------------------------------------------- /README.assets/ex16.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex16.2.png -------------------------------------------------------------------------------- /README.assets/ex16.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex16.3.png -------------------------------------------------------------------------------- /README.assets/ex17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex17.png -------------------------------------------------------------------------------- /README.assets/ex18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/ex18.png -------------------------------------------------------------------------------- /README.assets/hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/hierarchy.png -------------------------------------------------------------------------------- /README.assets/image-20190511230415520.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/image-20190511230415520.png -------------------------------------------------------------------------------- /README.assets/inputProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/inputProfile.png -------------------------------------------------------------------------------- /README.assets/kholerIllumination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/kholerIllumination.png -------------------------------------------------------------------------------- /README.assets/microscopeSimple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/microscopeSimple.png -------------------------------------------------------------------------------- /README.assets/outputProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/outputProfile.png -------------------------------------------------------------------------------- /README.assets/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/simple.png -------------------------------------------------------------------------------- /README.assets/simpleObjectImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/simpleObjectImage.png -------------------------------------------------------------------------------- /README.assets/thorlabs-0356707.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/thorlabs-0356707.png -------------------------------------------------------------------------------- /README.assets/thorlabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/README.assets/thorlabs.png -------------------------------------------------------------------------------- /docs/FAQ.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | ======================== 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_templates/autoClass.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :no-undoc-members: 7 | :show-inheritance: 8 | :inherited-members: 9 | 10 | {% block methods %} 11 | 12 | {% if methods %} 13 | .. rubric:: Methods 14 | 15 | .. autosummary:: 16 | :template: autoFunction.rst 17 | :toctree: methods/{{ objname }} 18 | 19 | {% for item in methods %} 20 | {%- if item not in inherited_members %} 21 | ~{{ name }}.{{ item }} 22 | {%- endif %} 23 | {%- endfor %} 24 | 25 | .. rubric:: Inherited Methods 26 | 27 | .. autosummary:: 28 | :template: autoInheritedFunction.rst 29 | :toctree: methods/{{ objname }} 30 | 31 | {% for item in methods %} 32 | {%- if item in inherited_members %} 33 | ~{{ name }}.{{ item }} 34 | {%- endif %} 35 | {%- endfor %} 36 | 37 | {% endif %} 38 | {% endblock %} 39 | 40 | {% block attributes %} 41 | {% if attributes %} 42 | .. rubric:: Attributes 43 | 44 | .. autosummary:: 45 | 46 | {% for item in attributes %} 47 | ~{{ name }}.{{ item }} 48 | {%- endfor %} 49 | {% endif %} 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /docs/_templates/autoFunction.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. autofunction:: {{ fullname }} 4 | :noindex: 5 | 6 | -------------------------------------------------------------------------------- /docs/_templates/autoInheritedFunction.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. autofunction:: {{ fullname }} 4 | :noindex: 5 | -------------------------------------------------------------------------------- /docs/_templates/classDefault.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | {% block methods %} 8 | .. automethod:: __init__ 9 | 10 | {% if methods %} 11 | .. rubric:: Methods 12 | 13 | .. autosummary:: 14 | {% for item in methods %} 15 | ~{{ name }}.{{ item }} 16 | {%- endfor %} 17 | {% endif %} 18 | {% endblock %} 19 | 20 | {% block attributes %} 21 | {% if attributes %} 22 | .. rubric:: Attributes 23 | 24 | .. autosummary:: 25 | {% for item in attributes %} 26 | ~{{ name }}.{{ item }} 27 | {%- endfor %} 28 | {% endif %} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /docs/_templates/moduleDefault.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block functions %} 6 | {% if functions %} 7 | .. rubric:: Functions 8 | 9 | .. autosummary:: 10 | {% for item in functions %} 11 | {{ item }} 12 | {%- endfor %} 13 | {% endif %} 14 | {% endblock %} 15 | 16 | {% block classes %} 17 | {% if classes %} 18 | .. rubric:: Classes 19 | 20 | .. autosummary:: 21 | {% for item in classes %} 22 | {{ item }} 23 | {%- endfor %} 24 | {% endif %} 25 | {% endblock %} 26 | 27 | {% block exceptions %} 28 | {% if exceptions %} 29 | .. rubric:: Exceptions 30 | 31 | .. autosummary:: 32 | {% for item in exceptions %} 33 | {{ item }} 34 | {%- endfor %} 35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | import sphinx_rtd_theme 16 | sys.path.insert(0, os.path.abspath('.')) 17 | sys.path.insert(0, os.path.abspath('..')) 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'RayTracing' 21 | copyright = '2020, DCC-Lab' 22 | author = 'DCC-Lab' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | autosummary_generate = True 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.napoleon', 'recommonmark', 'sphinx_rtd_theme'] 32 | autodoc_default_flags = ['members', 'show-inheritance'] 33 | autodoc_default_options = { 34 | "members": True, 35 | "inherited-members": False, 36 | "show-inheritance": True, 37 | } 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | html_theme = 'sphinx_rtd_theme' 53 | 54 | # Add any paths that contain custom static files (such as style sheets) here, 55 | # relative to this directory. They are copied after the builtin static files, 56 | # so a file named "default.css" will overwrite the builtin "default.css". 57 | html_static_path = ['_static'] 58 | 59 | # explicit master doc for ReadTheDocs build 60 | master_doc = 'index' 61 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Bug reporting 4 | 5 | If you found a bug and your code does not work you can follow these steps to report that: 6 | 7 | 8 | 9 | 1. Make sure you are using the latest version of the raytracing 10 | `pip install --upgrade raytracing` 11 | 12 | 13 | 14 | 2. Search for similar issues among the [Raytracing GitHub issues](https://github.com/DCC-Lab/RayTracing/issues). 15 | 16 | 17 | 18 | 3. Not solved? Open an issue on the [Raytracing GitHub](https://github.com/DCC-Lab/RayTracing/issues). 19 | Provide useful information: OS? Python version? 20 | 21 | 22 | 23 | 4. Explain your problem clearly: What is the error? Where is the problem? 24 | (Make sure to include a test code that shows the error) 25 | 26 | 27 | 28 | 5. Still having a problem? [e-mail](mailto:dccote%40cervo.ulaval.ca?subject=RaytracingPythonModule) us! 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/images/AngleHist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/AngleHist.png -------------------------------------------------------------------------------- /docs/images/Histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/Histogram.png -------------------------------------------------------------------------------- /docs/images/ImagingPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/ImagingPath.png -------------------------------------------------------------------------------- /docs/images/LambertianRays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/LambertianRays.png -------------------------------------------------------------------------------- /docs/images/RandomLambertianRays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/RandomLambertianRays.png -------------------------------------------------------------------------------- /docs/images/RandomUniformRays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/RandomUniformRays.png -------------------------------------------------------------------------------- /docs/images/UniformRays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/UniformRays.png -------------------------------------------------------------------------------- /docs/images/apertureStop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/apertureStop.png -------------------------------------------------------------------------------- /docs/images/displayRays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/displayRays.png -------------------------------------------------------------------------------- /docs/images/flipOrientation_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/flipOrientation_after.png -------------------------------------------------------------------------------- /docs/images/flipOrientation_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/flipOrientation_before.png -------------------------------------------------------------------------------- /docs/images/matrixGraphicDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/docs/images/matrixGraphicDisplay.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. raytracing documentation master file, created by 2 | sphinx-quickstart on Wed May 13 15:07:24 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | RayTracing 7 | ====================================== 8 | This code aims to provide a simple ray tracing module for calculating various 9 | properties of optical paths (object, image, aperture stops, field stops). It 10 | makes use of ABCD matrices and does not consider aberrations (spherical or 11 | chromatic). Since it uses the ABCD formalism (or Ray matrices, or Gauss 12 | matrices) it can perform tracing of rays and gaussian laser beams. 13 | 14 | A tutorial was published in the journal Neurophotonics. If you use this RayTracing module, please cite: 15 | 16 | **"Tools and tutorial on practical ray tracing for microscopy"**, by V. Pineau Noël¹, S. Masoumi¹, E. Parham¹, G. Genest, L. Bégin, M.-A. Vigneault, D. C. Côté, Neurophotonics, 8(1), 010801 (2021). ¹Equal contributions. https://doi.org/10.1117/1.NPh.8.1.010801 17 | 18 | This package is developed by `DCClab `_ members. The many contributions cannot be described in all their details, but a list of contributors is provided here in no particular order: 19 | 20 | * Elahe Parham: Documentation and examples writing 21 | * Shadi Masoumi: Tutorials, Examples and usage 22 | * Valérie Pineau Noël: Tutorials, Examples and usage 23 | * Gabriel Genest: Extensive Unit Testing and BugFinder Extraordinaire 24 | * Ludovick Bégin: Layout Artist Class Designer 25 | * Francois Côté: Bug finding, layout and Bob Ross look-alike 26 | * Mathieu Fournier: Unit Testing 27 | * Marc-André Vigneault: Examples 28 | * Daniel Côté: Official Designer & Merger, and "Gunnery Sergeant Hartman" Impersonator 29 | 30 | 31 | Contents 32 | ^^^^^^^^ 33 | 34 | .. toctree:: 35 | :maxdepth: 1 36 | 37 | raytracing 38 | gettingStarted 39 | reference 40 | examples 41 | contribute 42 | FAQ 43 | 44 | Indices and tables 45 | ^^^^^^^^^^^^^^^^^^ 46 | 47 | * :ref:`genindex` 48 | * :ref:`modindex` 49 | * :ref:`search` 50 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/raytracing.rst: -------------------------------------------------------------------------------- 1 | RayTracing 2 | ======================== 3 | 4 | 5 | This code aims to provide a simple ray tracing module for calculating various properties of optical paths (object, image, aperture stops, field stops). It makes use of ABCD matrices and does not consider aberrations (spherical or chromatic). Since it uses the ABCD formalism (or Ray matrices, or Gauss matrices) it can perform tracing of rays and gaussian laser beams. 6 | 7 | It is not a package to do "Rendering in 3D with raytracing". 8 | 9 | The code has been developed first for teaching purposes and is used in my "`Optique `_ 10 | " Study Notes (french only), but also for actual use in my research. There are `tutorials `_ (in french, with English subtitles) on YouTube. I have made no attempts at making high-performance code. **Readability** and **simplicity of usage** are the key here. It is a module with only a few files, and only matplotlib as a dependent module. 11 | 12 | The module defines ``Ray``, ``Matrix``, ``MatrixGroup`` and ``ImagingPath`` as the main elements for tracing rays. ``Matrix`` and ``MatrixGroup`` are either one or a sequence of many matrices into which ``Ray`` will propagate. ``ImagingPath`` is also a sequence of elements, with an object at the front edge. Specific subclasses of ``Matrix`` exists: ``Space``, ``Lens``, ``ThicklLens``, and ``Aperture``. Finally, a ray fan is a collection of rays, originating from a given point with a range of angles. 13 | 14 | If you want to perform calculations with coherent laser beams, then you use ``GaussianBeam`` and ``LaserPath``. Everything is essentially the same, except that the formalism does not allow for the gaussian beam to be "blocked", hence any calculation of stops with aperture is not available in ``LaserPath``. 15 | 16 | by [Daniel Côté](mailto:dccote@cervo.ulaval.ca?subject=Raytracing python module) -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | RayTracing API Reference 2 | ======================== 3 | .. automodule:: raytracing 4 | 5 | Matrix 6 | ------------------ 7 | 8 | .. autosummary:: 9 | :template: autoClass.rst 10 | :toctree: modules 11 | 12 | Matrix 13 | Aperture 14 | CurvedMirror 15 | DielectricInterface 16 | DielectricSlab 17 | Lens 18 | Space 19 | ThickLens 20 | 21 | MatrixGroup 22 | ------------------ 23 | 24 | .. autosummary:: 25 | :template: autoClass.rst 26 | :toctree: modules 27 | 28 | MatrixGroup 29 | 30 | ImagingPath 31 | ------------------ 32 | 33 | .. autosummary:: 34 | :template: autoClass.rst 35 | :toctree: modules 36 | 37 | ImagingPath 38 | 39 | Ray 40 | ------------------ 41 | 42 | .. autosummary:: 43 | :template: autoClass.rst 44 | :toctree: modules 45 | 46 | Ray 47 | 48 | Rays 49 | ------------------ 50 | 51 | .. autosummary:: 52 | :template: autoClass.rst 53 | :toctree: modules 54 | 55 | Rays 56 | ObjectRays 57 | UniformRays 58 | RandomRays 59 | LambertianRays 60 | RandomLambertianRays 61 | RandomUniformRays 62 | 63 | Laser Path 64 | ------------------ 65 | 66 | .. autosummary:: 67 | :template: autoClass.rst 68 | :toctree: modules 69 | 70 | LaserPath 71 | 72 | Gaussian Beam 73 | ------------------ 74 | 75 | .. autosummary:: 76 | :template: autoClass.rst 77 | :toctree: modules 78 | 79 | GaussianBeam 80 | 81 | Components 82 | ------------------ 83 | 84 | .. autosummary:: 85 | :template: autoClass.rst 86 | :toctree: modules 87 | 88 | System2f 89 | System4f 90 | 91 | Axicon 92 | ------------------ 93 | 94 | .. autosummary:: 95 | :template: autoClass.rst 96 | :toctree: modules 97 | 98 | Axicon 99 | 100 | Specialty Lenses 101 | ------------------ 102 | 103 | .. autosummary:: 104 | :template: autoClass.rst 105 | :toctree: modules 106 | 107 | AchromatDoubletLens 108 | Objective 109 | 110 | Olympus 111 | ------------------ 112 | 113 | .. autosummary:: 114 | :template: autoClass.rst 115 | :toctree: modules 116 | 117 | olympus.LUMPlanFL40X 118 | olympus.XLUMPlanFLN20X 119 | olympus.MVPlapo2XC 120 | olympus.UMPLFN20XW 121 | olympus.XLPLN25X 122 | 123 | Edmund Optics 124 | ------------------ 125 | 126 | .. autosummary:: 127 | :template: autoClass.rst 128 | :toctree: modules 129 | 130 | eo.PN_33_921 131 | eo.PN_33_922 132 | eo.PN_88_593 133 | eo.PN_85_877 134 | 135 | Nikon 136 | ------------------ 137 | 138 | .. autosummary:: 139 | :template: autoClass.rst 140 | :toctree: modules 141 | 142 | nikon.LWD16X 143 | 144 | Thorlabs 145 | ------------------ 146 | 147 | .. autosummary:: 148 | :template: autoClass.rst 149 | :toctree: modules 150 | 151 | thorlabs.ACN254_100_A 152 | thorlabs.ACN254_075_A 153 | thorlabs.ACN254_050_A 154 | thorlabs.ACN254_040_A 155 | thorlabs.AC254_030_A 156 | thorlabs.AC254_035_A 157 | thorlabs.AC254_040_A 158 | thorlabs.AC254_045_A 159 | thorlabs.AC254_050_A 160 | thorlabs.AC254_060_A 161 | thorlabs.AC254_075_A 162 | thorlabs.AC254_080_A 163 | thorlabs.AC254_100_A 164 | thorlabs.AC254_125_A 165 | thorlabs.AC254_150_A 166 | thorlabs.AC254_200_A 167 | thorlabs.AC254_250_A 168 | thorlabs.AC254_300_A 169 | thorlabs.AC254_400_A 170 | thorlabs.AC254_500_A 171 | thorlabs.AC254_050_B 172 | thorlabs.AC508_075_B 173 | thorlabs.AC508_080_B 174 | thorlabs.AC508_100_B 175 | thorlabs.AC508_150_B 176 | thorlabs.AC508_200_B 177 | thorlabs.AC508_250_B 178 | thorlabs.AC508_300_B 179 | thorlabs.AC508_400_B 180 | thorlabs.AC508_500_B 181 | thorlabs.AC508_750_B 182 | thorlabs.AC508_1000_B 183 | thorlabs.LA1608_A 184 | thorlabs.LA1134_A 185 | thorlabs.LA1131_A 186 | thorlabs.LA1422_A 187 | thorlabs.LA1805_A 188 | thorlabs.LA1274_A 189 | 190 | Materials 191 | ------------------ 192 | 193 | .. autosummary:: 194 | :template: autoClass.rst 195 | :toctree: modules 196 | 197 | Material 198 | Air 199 | N_BK7 200 | N_SF2 201 | SF2 202 | SF5 203 | N_SF5 204 | N_SF6HT 205 | N_SF10 206 | N_SF11 207 | N_BAF10 208 | E_BAF11 209 | N_BAK1 210 | N_BAK4 211 | FK51A 212 | LAFN7 213 | N_LASF9 214 | N_LAK22 215 | N_SSK5 216 | E_FD10 217 | FusedSilica 218 | 219 | Zemax reader 220 | ------------------ 221 | 222 | .. autosummary:: 223 | :template: autoClass.rst 224 | :toctree: modules 225 | 226 | ZMXReader -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme==0.4.3 2 | -------------------------------------------------------------------------------- /docs/webgraphviz_url/site.js: -------------------------------------------------------------------------------- 1 | // Delay loading any function until the html dom has loaded. All functions are 2 | // defined in this top level function to ensure private scope. 3 | jQuery(document).ready(function () { 4 | 5 | // Installs error handling. 6 | jQuery.ajaxSetup({ 7 | error: function(resp, e) { 8 | if (resp.status == 0){ 9 | alert('You are offline!!\n Please Check Your Network.'); 10 | } else if (resp.status == 404){ 11 | alert('Requested URL not found.'); 12 | } else if (resp.status == 500){ 13 | alert('Internel Server Error:\n\t' + resp.responseText); 14 | } else if (e == 'parsererror') { 15 | alert('Error.\nParsing JSON Request failed.'); 16 | } else if (e == 'timeout') { 17 | alert('Request timeout.'); 18 | } else { 19 | alert('Unknown Error.\n' + resp.responseText); 20 | } 21 | } 22 | }); // error:function() 23 | 24 | 25 | var generate_btn = jQuery('#generate_btn'); 26 | var sample_1_btn = jQuery('#sample_1_btn'); 27 | var sample_2_btn = jQuery('#sample_2_btn'); 28 | var sample_3_btn = jQuery('#sample_3_btn'); 29 | var sample_4_btn = jQuery('#sample_4_btn'); 30 | var sample_5_btn = jQuery('#sample_5_btn'); 31 | 32 | var svg_div = jQuery('#graphviz_svg_div'); 33 | var graphviz_data_textarea = jQuery('#graphviz_data'); 34 | 35 | function InsertGraphvizText(text) { 36 | graphviz_data_textarea.val(text); 37 | } 38 | 39 | 40 | function UpdateGraphviz() { 41 | svg_div.html(""); 42 | var data = graphviz_data_textarea.val(); 43 | // Generate the Visualization of the Graph into "svg". 44 | var svg = Viz(data, "svg"); 45 | svg_div.html("
"+svg); 46 | } 47 | 48 | // Startup function: call UpdateGraphviz 49 | jQuery(function() { 50 | // The buttons are disabled, enable them now that this script 51 | // has loaded. 52 | generate_btn.removeAttr("disabled") 53 | .text("Generate Graph!"); 54 | 55 | sample_1_btn.removeAttr("disabled"); 56 | sample_2_btn.removeAttr("disabled"); 57 | sample_3_btn.removeAttr("disabled"); 58 | sample_4_btn.removeAttr("disabled"); 59 | sample_5_btn.removeAttr("disabled"); 60 | }); 61 | 62 | // Bind actions to form buttons. 63 | generate_btn.click(UpdateGraphviz); 64 | 65 | sample_1_btn.click(function(){ 66 | InsertGraphvizText(jQuery("#sample1_text").html().trim()); 67 | }); 68 | 69 | sample_2_btn.click(function(){ 70 | InsertGraphvizText(jQuery("#sample2_text").html().trim()); 71 | }); 72 | 73 | sample_3_btn.click(function(){ 74 | InsertGraphvizText(jQuery("#sample3_text").html().trim()); 75 | }); 76 | 77 | sample_4_btn.click(function(){ 78 | InsertGraphvizText(jQuery("#sample4_text").html().trim()); 79 | }); 80 | 81 | sample_5_btn.click(function(){ 82 | InsertGraphvizText(jQuery("#sample5_text").html().trim()); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples (moved) 2 | 3 | by Prof. [Daniel Côté](mailto:dccote@cervo.ulaval.ca?subject=Raytracing%20python%20module) and his group http://www.dcclab.ca 4 | 5 | The publication: 6 | 7 | > ["Tools and tutorial on practical ray tracing for microscopy"](https://doi.org/10.1117/1.NPh.8.1.010801) 8 | > 9 | > by V. Pineau Noël*, S. Masoumi*, E. Parham*, G. Genest, L. Bégin, M.-A. Vigneault, D. C. Côté, 10 | > Neurophotonics, 8(1), 010801 (2021). 11 | > *Equal contributions. 12 | > Permalink: https://doi.org/10.1117/1.NPh.8.1.010801 13 | 14 | has a link to this directory in its text, but all examples have been moved inside the module and can be found [here](../raytracing/examples/). 15 | 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml is the newer method for module distribution 2 | # setup.py is not needed, you simply do python -m build 3 | # 4 | # To distribute: 5 | # ============= 6 | # rm dist/*; python -m build; python -m twine upload dist/* 7 | 8 | [build-system] 9 | requires = ["setuptools>=61", "setuptools_scm>=7", "wheel"] 10 | build-backend = "setuptools.build_meta" 11 | 12 | [project] 13 | name = "raytracing" 14 | # 15 | # Version __version__ will be loaded by setuptools_scm using a git tag 16 | # This requires a git tag in the form of v.1.3.13 17 | dynamic = ["version"] 18 | description = "Simple optical ray tracing library to validate the design of an optical system." 19 | readme = { file = "README.md", content-type = "text/markdown" } 20 | requires-python = ">=3.6" 21 | license = { text = "MIT" } 22 | 23 | authors = [ 24 | { name = "Daniel Cote", email = "dccote@cervo.ulaval.ca" } 25 | ] 26 | 27 | urls = { Homepage = "https://github.com/DCC-Lab/RayTracing" } 28 | 29 | keywords = [ 30 | "optics", "optical", "lens", "ray tracing", "matrix", "matrices", "aperture", "field stop", 31 | "monte carlo", "design", "raytracing", "zemax", "chromatic aberrations" 32 | ] 33 | 34 | classifiers = [ 35 | "Development Status :: 5 - Production/Stable", 36 | "Intended Audience :: Science/Research", 37 | "Intended Audience :: Education", 38 | "Topic :: Scientific/Engineering :: Physics", 39 | "Topic :: Scientific/Engineering :: Visualization", 40 | "Topic :: Education", 41 | "License :: OSI Approved :: MIT License", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.6", 44 | "Programming Language :: Python :: 3.7", 45 | "Programming Language :: Python :: 3.8", 46 | "Programming Language :: Python :: 3.9", 47 | "Programming Language :: Python :: 3.10", 48 | "Programming Language :: Python :: 3.11", 49 | "Programming Language :: Python :: 3.12", 50 | "Programming Language :: Python :: 3.13", 51 | "Operating System :: OS Independent" 52 | ] 53 | 54 | dependencies = [ 55 | "matplotlib>=3.7", 56 | "numpy", 57 | "pygments", 58 | "mytk" 59 | ] 60 | 61 | [tool.setuptools_scm] 62 | version_scheme = "post-release" 63 | # This is apparently needed because ptyhon -m build loses the .git tags 64 | write_to = "raytracing/_version.py" 65 | 66 | [tool.setuptools] 67 | package-dir = {"" = "."} 68 | include-package-data = true 69 | 70 | [tool.setuptools.packages.find] 71 | where = ["."] 72 | include = ["raytracing*"] 73 | 74 | [tool.setuptools.package-data] 75 | raytracing = ["*.png"] 76 | -------------------------------------------------------------------------------- /raytracing/__init__.py: -------------------------------------------------------------------------------- 1 | r"""A simple module for ray tracing with ABCD matrices. 2 | https://github.com/DCC-Lab/RayTracing 3 | 4 | Create an `ImagingPath()`, append matrices (optical elements or other 5 | group of elements), and then `display()`. This helps determine of 6 | course simple things like focal length of compound systems, 7 | object-image, etc... but also the aperture stop, field stop, field 8 | of view and any clipping issues that may occur. 9 | 10 | When displaying the result with an `ImagingPath()`, the `objectHeight`, 11 | `fanAngle`, and `fanNumber` are used if the field of view is not 12 | defined. You may adjust the values to suit your needs in `ImagingPath()`. 13 | 14 | Create a `LaserPath()` to analyse gaussian beams using the complex radius 15 | of curvature q and the same matrices. 16 | 17 | The class hierarchy can be obtained with `python -m raytracing --classes` 18 | """ 19 | 20 | import math 21 | 22 | """ We import almost everything by default, in the general namespace because it is simpler for everyone """ 23 | 24 | """ General matrices and groups for tracing rays and gaussian beams""" 25 | from .matrix import * 26 | from .matrixgroup import * 27 | 28 | """ Ray matrices for geometrical optics """ 29 | from .ray import * 30 | from .rays import * 31 | from .imagingpath import * 32 | 33 | """ ABCD matrices for gaussian beams """ 34 | from .gaussianbeam import * 35 | from .laserpath import * 36 | from .lasercavity import * 37 | 38 | """ Matrices for components: System4f (synonym: Telescope), System2f """ 39 | from .components import * 40 | 41 | """ Specialty lenses : objectives and achromats, but we keep the namespace for the vendor lenses """ 42 | from .specialtylenses import * 43 | from .axicon import * 44 | 45 | from . import thorlabs 46 | from . import eo 47 | from . import olympus 48 | 49 | from .zemax import * 50 | 51 | from .utils import * 52 | from .preferences import * 53 | 54 | import os 55 | from datetime import datetime 56 | 57 | """ Synonym of Matrix: Element 58 | 59 | We can use a mathematical language (Matrix) or optics terms (Element) 60 | """ 61 | Element = Matrix 62 | Group = MatrixGroup 63 | OpticalPath = ImagingPath 64 | 65 | try: # to get __version__ dynamically from pyproject.toml 66 | from importlib.metadata import version 67 | except ImportError: # Python <3.8 68 | from importlib_metadata import version 69 | 70 | from ._version import version as __version__ 71 | 72 | __author__ = "Daniel Cote " 73 | 74 | import os.path as path 75 | import time 76 | import tempfile 77 | 78 | 79 | def lastCheckMoreThanADay(): 80 | if "lastVersionCheck" in prefs: 81 | isoFormat = "%Y-%m-%dT%H:%M:%S.%f" 82 | then = datetime.strptime(prefs["lastVersionCheck"], isoFormat) 83 | difference = datetime.now() - then 84 | if difference.days > 1: 85 | return True 86 | else: 87 | return False 88 | else: 89 | return True 90 | 91 | 92 | prefs = Preferences() 93 | if lastCheckMoreThanADay(): 94 | checkLatestVersion(currentVersion=__version__) 95 | prefs["lastVersionCheck"] = datetime.now().isoformat() 96 | 97 | if "RAYTRACING_EXPERT" in os.environ: 98 | prefs["mode"] = "expert" 99 | 100 | try: 101 | if "mode" in prefs: 102 | if prefs["mode"] == "silent": 103 | silentMode() 104 | elif prefs["mode"] == "expert": 105 | expertMode() 106 | else: 107 | beginnerMode(saveToPrefs=False) 108 | else: 109 | beginnerMode(saveToPrefs=True) 110 | 111 | except Exception as err: 112 | beginnerMode(saveToPrefs=True) 113 | pass 114 | -------------------------------------------------------------------------------- /raytracing/__main__.py: -------------------------------------------------------------------------------- 1 | from .imagingpath import * 2 | from .laserpath import * 3 | from .lasercavity import * 4 | 5 | from .specialtylenses import * 6 | from .axicon import * 7 | from . import thorlabs 8 | from . import eo 9 | from . import olympus 10 | from . import utils 11 | 12 | import os 13 | import sys 14 | import argparse 15 | import re 16 | 17 | from io import BytesIO 18 | from PIL import Image 19 | import matplotlib.pyplot as plt 20 | from . import examples # 'all' will gather all example files dynamically 21 | import subprocess 22 | 23 | # We start by figuring out what the user really wants. If they don't know, 24 | # we offer some help 25 | ap = argparse.ArgumentParser(prog="python -m raytracing") 26 | ap.add_argument( 27 | "-a", 28 | "--app", 29 | action="store_true", 30 | help="Start the graphical user interface", 31 | ) 32 | ap.add_argument( 33 | "-e", 34 | "--examples", 35 | required=False, 36 | default="all", 37 | help="Specific example numbers, separated by a comma", 38 | ) 39 | ap.add_argument( 40 | "-c", 41 | "--classes", 42 | required=False, 43 | action="store_true", 44 | help="Print the class hierarchy in graphviz format", 45 | ) 46 | ap.add_argument( 47 | "-l", 48 | "--list", 49 | required=False, 50 | action="store_const", 51 | const=True, 52 | help="List all the accessible examples", 53 | ) 54 | ap.add_argument( 55 | "-t", 56 | "--tests", 57 | required=False, 58 | action="store_true", 59 | help="Run all Unit tests", 60 | ) 61 | 62 | args = vars(ap.parse_args()) 63 | runApp = args["app"] 64 | runExamples = args["examples"] 65 | runTests = args["tests"] 66 | printClasses = args["classes"] 67 | listExamples = args["list"] 68 | 69 | if runApp: 70 | # Build path to gui_app.py 71 | current_dir = os.path.dirname(__file__) 72 | gui_path = os.path.join(current_dir, "ui", "raytracing_app.py") 73 | 74 | # Call the GUI app as a subprocess 75 | subprocess.run([sys.executable, gui_path]) 76 | exit() 77 | 78 | if runExamples == "all": 79 | runExamples = range(1, len(examples.short) + 1) 80 | elif runExamples == "": 81 | runExamples = [] 82 | else: 83 | runExamples = [int(y) for y in runExamples.split(",")] 84 | 85 | if printClasses: 86 | printClassHierarchy(Rays) 87 | printClassHierarchy(Matrix) 88 | elif runTests: 89 | moduleDir = os.path.dirname(os.path.realpath(__file__)) 90 | err = os.system("cd {0}/tests; {1} -m unittest".format(moduleDir, sys.executable)) 91 | elif listExamples: 92 | topDir = os.path.dirname(os.path.realpath(examples.__file__)) 93 | all = [] 94 | all.extend(examples.short) 95 | all.extend(examples.short) 96 | 97 | print("\nAll examples") 98 | print("==============") 99 | for i, entry in enumerate(examples.short): 100 | print("{0:2d}. {1}.py {2}".format(i + 1, entry["name"], entry["title"])) 101 | 102 | print("\nMore examples code available in: {0}".format(topDir)) 103 | elif runExamples: 104 | # Some decent parameters for plots 105 | # See https://matplotlib.org/api/font_manager_api.html#matplotlib.font_manager.FontProperties.set_size 106 | params = { 107 | "legend.fontsize": "x-large", 108 | "figure.figsize": (10, 7), 109 | "axes.labelsize": "x-large", 110 | "axes.titlesize": "x-large", 111 | "xtick.labelsize": "x-large", 112 | "ytick.labelsize": "x-large", 113 | "font.family": "helvetica", 114 | } 115 | plt.rcParams.update(params) 116 | 117 | print("Running example code : {0}".format(runExamples)) 118 | for i in runExamples: 119 | entry = examples.short[i - 1] 120 | print("\nScript '{0}.py' - begin source code".format(entry["name"])) 121 | print(entry["terminalSourceCode"], end="") 122 | print("\nScript '{0}.py' - end source code".format(entry["name"])) 123 | print("\nScript '{0}.py' - begin output".format(entry["name"])) 124 | entry["code"](comments=entry["bmpSourceCode"]) 125 | print("Script '{0}.py' - end output".format(entry["name"])) 126 | -------------------------------------------------------------------------------- /raytracing/axicon.py: -------------------------------------------------------------------------------- 1 | from .matrix import * 2 | import matplotlib.pyplot as plt 3 | 4 | 5 | class Axicon(Matrix): 6 | """ 7 | This class is an advanced module that describes an axicon lens, 8 | not part of the basic formalism. Using this class an axicon 9 | conical lens can be presented. 10 | Axicon lenses are used to obtain a line focus instead of a point. 11 | The formalism is described in Kloos, sec. 2.2.3 12 | 13 | Parameters 14 | ---------- 15 | alpha : float 16 | alpha is the small angle in radians of the axicon 17 | (typically 2.5 or 5 degrees) corresponding to 90-apex angle 18 | n : float 19 | index of refraction. 20 | This value cannot be less than 1.0. It is assumed the axicon 21 | is in air. 22 | diameter : float 23 | Aperture of the element. (default = +Inf) 24 | The diameter of the aperture must be a positive value. 25 | label : string 26 | The label of the axicon lens. 27 | 28 | """ 29 | 30 | def __init__(self, alpha, n, diameter=float('+Inf'), label=''): 31 | 32 | self.n = n 33 | self.alpha = alpha 34 | super(Axicon, self).__init__(A=1, B=0, C=0, D=1, physicalLength=0, apertureDiameter=diameter, label=label, 35 | frontIndex=1.0, backIndex=1.0) 36 | 37 | def deviationAngle(self): 38 | """ This function provides deviation angle delta assuming that 39 | the axicon is in air and that the incidence is near normal, 40 | which is the usual way of using an axicon. 41 | 42 | Returns 43 | ------- 44 | delta : float 45 | the deviation angle 46 | 47 | See ALso 48 | -------- 49 | https://ru.b-ok2.org/book/2482970/9062b7, p.48 50 | 51 | """ 52 | 53 | return (self.n - 1.0) * self.alpha 54 | 55 | def focalLineLength(self, yMax=None): 56 | """ Provides the line length, assuming a ray at height yMax 57 | 58 | Parameters 59 | ---------- 60 | yMax : float 61 | the height of the ray (default=None) 62 | If no height is defined for the ray, then yMax would be set to the height of the axicon (apertureDiameter/2) 63 | 64 | Returns 65 | ------- 66 | focalLineLength : float 67 | the length of the focal line 68 | 69 | See ALso 70 | -------- 71 | https://ru.b-ok2.org/book/2482970/9062b7, p.48 72 | 73 | """ 74 | 75 | if yMax == None: 76 | yMax = self.apertureDiameter / 2 77 | 78 | return abs(yMax) / (self.n - 1.0) / self.alpha 79 | 80 | def mul_ray(self, rightSideRay): 81 | """ This function is used to calculate the output ray through an axicon. 82 | 83 | Parameters 84 | ---------- 85 | rightSideRay : object of ray class 86 | A ray with a defined height and angle. 87 | 88 | Returns 89 | ------- 90 | outputRay : object of ray class 91 | the height and angle of the output ray. 92 | 93 | See Also 94 | -------- 95 | raytracing.Matrix.mul_ray 96 | 97 | 98 | """ 99 | 100 | outputRay = super(Axicon, self).mul_ray(rightSideRay) 101 | 102 | if rightSideRay.y > 0: 103 | outputRay.theta += -self.deviationAngle() 104 | elif rightSideRay.y < 0: 105 | outputRay.theta += self.deviationAngle() 106 | # theta == 0 is not deviated 107 | 108 | return outputRay 109 | 110 | def mul_beam(self, rightSideBeam): 111 | """This function calculates the multiplication of a coherent beam with complex radius 112 | of curvature q by an ABCD matrix. However it will raise an error in case the input is an axicon 113 | 114 | Parameters 115 | ---------- 116 | rightSideBeam : object from GaussianBeam class 117 | including the beam properties 118 | 119 | 120 | Returns 121 | ------- 122 | outputBeam : object from GaussianBeam class 123 | The properties of the beam at the output of the system with the defined ABCD matrix 124 | 125 | See Also 126 | -------- 127 | raytracing.Matrix.mul_matrix 128 | raytracing.Matrix.mul_ray 129 | raytracing.GaussianBeam 130 | """ 131 | 132 | raise TypeError("Cannot use Axicon with GaussianBeam, only with Ray") 133 | 134 | @property 135 | def forwardSurfaces(self): 136 | """ A list of surfaces that represents the element for drawing purposes 137 | """ 138 | minThickness = - np.tan(self.alpha) * self.displayHalfHeight() 139 | return [FlatInterface(n=self.n, L=minThickness), 140 | ConicalInterface(alpha=self.alpha)] 141 | -------------------------------------------------------------------------------- /raytracing/components.py: -------------------------------------------------------------------------------- 1 | from .matrix import * 2 | from .matrixgroup import * 3 | from math import * 4 | 5 | class System4f(MatrixGroup): 6 | """ 7 | The matrix group of a 4f system can be defined using this function. 8 | 9 | Parameters 10 | ---------- 11 | f1 : float 12 | The focal length of the first lens 13 | f2 : float 14 | The focal length of the second lens 15 | diameter1 : float 16 | The diameter of the first lens. This value must be positive. (default=+Inf) 17 | diameter2 : float 18 | The diameter of the second lens. This value must be positive. (default=+Inf) 19 | label : string 20 | The label for the 4f system 21 | 22 | """ 23 | def __init__(self,f1, f2, diameter1=float('+Inf'), diameter2=float('+Inf'), label=''): 24 | elements = [] 25 | elements.append(Space(d=f1)) 26 | elements.append(Lens(f=f1, diameter=diameter1)) 27 | elements.append(Space(d=f1)) 28 | elements.append(Space(d=f2)) 29 | elements.append(Lens(f=f2, diameter=diameter2)) 30 | elements.append(Space(d=f2)) 31 | super(Telescope, self).__init__(elements=elements, label=label) 32 | 33 | 34 | class System2f(MatrixGroup): 35 | """ 36 | The matrix group of a 2f system can be defined using this function. 37 | 38 | Parameters 39 | ---------- 40 | f : float 41 | The focal length of the lens 42 | diameter : float 43 | The diameter of the lens. This value must be positive. (default=+Inf) 44 | label : string 45 | The label for the 2f system 46 | 47 | """ 48 | def __init__(self,f, diameter=float('+Inf'), label=''): 49 | elements = [] 50 | elements.append(Space(d=f)) 51 | elements.append(Lens(f=f, diameter=diameter)) 52 | elements.append(Space(d=f)) 53 | super(System2f, self).__init__(elements=elements, label=label) 54 | 55 | Telescope = System4f 56 | 57 | -------------------------------------------------------------------------------- /raytracing/eo.py: -------------------------------------------------------------------------------- 1 | from .specialtylenses import * 2 | 3 | class PN_33_921(AchromatDoubletLens): 4 | """PN_33_921 5 | 6 | .. csv-table:: 7 | :header: Parameter, value 8 | 9 | "fa", "100.0" 10 | "fb", "78.32" 11 | "R1", "64.67" 12 | "R2", "-64.67" 13 | "R3", "-343.59" 14 | "tc1", "26.0" 15 | "tc2", "12.7" 16 | "te", "24.66" 17 | "n1", "1.6700" 18 | "n2", "1.8467" 19 | "diameter", "75" 20 | 21 | """ 22 | def __init__(self): 23 | # PN for Part number 24 | super(PN_33_921,self).__init__(fa=100.00,fb=78.32, R1=64.67,R2=-64.67, R3=-343.59, 25 | tc1=26.00, tc2=12.7, te=24.66, n1=1.6700, n2=1.8467, diameter=75, 26 | label="EO #33-921", 27 | url="https://www.edmundoptics.com/p/75mm-dia-x-100mm-fl-vis-0-coated-achromatic-lens/3374/") 28 | 29 | class PN_33_922(AchromatDoubletLens): 30 | """PN_33_922 31 | 32 | .. csv-table:: 33 | :header: Parameter, value 34 | 35 | "fa", "150.0" 36 | "fb", "126.46" 37 | "R1", "92.05" 38 | "R2", "-72.85" 39 | "R3", "-305.87" 40 | "tc1", "23.2" 41 | "tc2", "23.1" 42 | "te", "36.01" 43 | "n1", "0.5876" 44 | "n2", "0.5876" 45 | "diameter", "75" 46 | 47 | """ 48 | def __init__(self): 49 | # PN for Part number 50 | super(PN_33_922,self).__init__(fa=150.00,fb=126.46, R1=92.05,R2=-72.85, R3=-305.87, 51 | tc1=23.2, tc2=23.1, te=36.01, n1=None, mat1=N_BAK1, n2=None, mat2=N_SF8, diameter=75, 52 | label="EO #33-922", 53 | url="https://www.edmundoptics.com/p/75mm-dia-x-150mm-fl-vis-0-coated-achromatic-lens/3376/", 54 | wavelengthRef=0.5876) 55 | 56 | class PN_88_593(AchromatDoubletLens): 57 | """PN_88_593 58 | 59 | .. csv-table:: 60 | :header: Parameter, value 61 | 62 | "fa", "200.0" 63 | "fb", "187.69" 64 | "R1", "118.81" 65 | "R2", "-96.37" 66 | "R3", "-288.97" 67 | "tc1", "17.94" 68 | "tc2", "6.00" 69 | "te", "15.42" 70 | "n1", "1.5168" 71 | "n2", "1.6727" 72 | "diameter", "75" 73 | 74 | """ 75 | def __init__(self): 76 | super(PN_88_593,self).__init__(fa=200.00,fb=187.69, R1=118.81, R2=-96.37, R3=-288.97, 77 | tc1=17.94, tc2=6.00, te=15.42, n1=1.5168, n2=1.6727, diameter=75, 78 | label="EO #88-593", 79 | url="https://www.edmundoptics.com/p/75mm-dia-x-200mm-fl-vis-0deg-coated-achromatic-lens/30844/") 80 | 81 | class PN_85_877(AchromatDoubletLens): 82 | """PN_85_877 83 | 84 | .. csv-table:: 85 | :header: Parameter, value 86 | 87 | "fa", "-10.0" 88 | "fb", "-11.92" 89 | "R1", "-6.55" 90 | "R2", "-5.10" 91 | "R3", "89.10" 92 | "tc1", "1.0" 93 | "tc2", "2.5" 94 | "te", "4.2" 95 | "n1", "0.5876" 96 | "n2", "0.5876" 97 | "diameter", "6.25" 98 | 99 | """ 100 | def __init__(self): 101 | super(PN_85_877,self).__init__(fa=-10.0,fb=-11.92, R1=-6.55, R2=5.10, R3=89.10, 102 | tc1=1.0, tc2=2.5, te=4.2, n1=None, mat1=N_BAF10, n2=None, mat2=N_SF10, diameter=6.25, 103 | label="EO #85-877", 104 | url="https://www.edmundoptics.com/p/625mm-dia-x10mm-fl-vis-nir-coated-negative-achromatic-lens/28478/", 105 | wavelengthRef=0.5876) 106 | -------------------------------------------------------------------------------- /raytracing/examples/README.md: -------------------------------------------------------------------------------- 1 | # RayTracing examples 2 | 3 | by Prof. [Daniel Côté](mailto:dccote@cervo.ulaval.ca?subject=Raytracing%20python%20module) and his group http://www.dcclab.ca 4 | 5 | The publication is available here: 6 | 7 | > ["Tools and tutorial on practical ray tracing for microscopy"](https://doi.org/10.1117/1.NPh.8.1.010801) 8 | > 9 | > by V. Pineau Noël*, S. Masoumi*, E. Parham*, G. Genest, L. Bégin, M.-A. Vigneault, D. C. Côté, 10 | > Neurophotonics, 8(1), 010801 (2021). 11 | > *Equal contributions. 12 | > Permalink: https://doi.org/10.1117/1.NPh.8.1.010801 13 | 14 | ## Listing the examples 15 | 16 | The present directory contains a large number of examples. Some are short and show how to use the raytracing module for simple tasks. They start with the prefix `ex`. Others are longer and perform more than just a simple trace and display a graph: they use Monte Carlo, the get values from functions and perform other calculations, etc... Finally, the scripts that generated the figures from the article cited above start with the prefix `fig` and end with `.py`. Finally, some files are simply the actual figures (`.png`) if they were not calculated. 17 | 18 | This will show you a list of examples of things you can do: 19 | 20 | ```shell 21 | python -m raytracing -l # List examples 22 | python -m raytracing -e all # Run all of them 23 | python -m raytracing -e 1,2,4,6 # Only run 1,2,4 and 6 24 | python -m raytracing -f 5,6,7. # Run scripts that generated figures 5,6 and 7 in article 25 | ``` 26 | 27 | or request help with: 28 | 29 | ```shell 30 | python -m raytracing -h 31 | ``` 32 | 33 | ## Licence 34 | 35 | This code is provided under the [MIT License](./LICENSE). -------------------------------------------------------------------------------- /raytracing/examples/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import importlib 4 | 5 | from pygments import highlight 6 | from pygments.lexers import PythonLexer 7 | from pygments.formatters import TerminalFormatter, BmpImageFormatter 8 | 9 | from PIL import Image 10 | from io import BytesIO 11 | 12 | """ This module is only here to support examples, so it dynamically 13 | loads all files that appear to be example files from the directory. 14 | We get the title, the description and the entry function. 15 | 16 | TODO: all other examples from the article are in the diretory 17 | but are not added to the list because they have a structure 18 | that is different (no TITLE and not constrined to a single function). 19 | """ 20 | topDir = os.path.dirname(os.path.realpath(__file__)) 21 | allFiles = os.listdir(topDir) 22 | allFiles.sort() 23 | 24 | short = [] 25 | long = [] 26 | for file in allFiles: 27 | pattern = r'^(ex\d+|fig.+)\.py$' 28 | matchObj = re.match(pattern, file) 29 | if matchObj: 30 | name = matchObj.group(1) 31 | module = importlib.import_module(".{0}".format(name),package="raytracing.examples") 32 | with open("{0}/{1}".format(topDir, file)) as f: 33 | srcCode = f.readlines() 34 | # The last three lines are always the main() call 35 | srcCode = srcCode[:-3] 36 | srcCode = str.join('', srcCode) 37 | module.__SRC_CODE = srcCode 38 | 39 | bmpSrcCode = highlight(srcCode, PythonLexer(), BmpImageFormatter()) 40 | module.__IMG_CODE = Image.open(BytesIO(bmpSrcCode)) 41 | 42 | short.append({"name":name, 43 | "title":module.TITLE, 44 | "code":module.exampleCode, 45 | "sourceCode":srcCode, 46 | "terminalSourceCode":highlight(srcCode, PythonLexer(), TerminalFormatter()), 47 | "bmpSourceCode":Image.open(BytesIO(bmpSrcCode)), 48 | "path":"{0}/{1}".format(topDir, file) 49 | }) 50 | else: 51 | matchObj = re.match(r'^(.+)\.py$', file) 52 | if matchObj: 53 | # Other more complete examples: 54 | name = matchObj.group(1) 55 | if name in ["__init__","template", "envexamples"]: 56 | continue 57 | 58 | with open("{0}/{1}".format(topDir, file)) as f: 59 | srcCode = f.readlines() 60 | 61 | srcCode = str.join('', srcCode) 62 | bmpSrcCode = highlight(srcCode, PythonLexer(), BmpImageFormatter()) 63 | 64 | long.append({"name":name, 65 | "sourceCode":srcCode, 66 | "terminalSourceCode":highlight(srcCode, PythonLexer(), TerminalFormatter()), 67 | "bmpSourceCode":Image.open(BytesIO(bmpSrcCode)), 68 | "path":"{0}/{1}".format(topDir, file) 69 | }) 70 | -------------------------------------------------------------------------------- /raytracing/examples/confocalDetection.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | 3 | from raytracing import * 4 | import matplotlib.pyplot as plt 5 | 6 | """ To obtain and plot the intensity of a point source at the pinhole of a 7 | confocal microscope (with variable pinhole size) as a function of position of 8 | focal spot by sending a large number of rays in the system (changing the 9 | position of the focal spot provides an optical sectioning process). """ 10 | 11 | # Focal spot radius (Airy disk radius) 12 | focalRadius = 0.000250 13 | # Dictionary of pinhole factors with an empty list which will subsequently 14 | # contain the transmission efficiency for each focal spot position 15 | pinholeModifier = {1 / 3: [], 1: [], 3: []} 16 | # list of all relative positions from the ideal focal spot position in nm 17 | positions = [1000, 800, 500, 300, 150, 100, 50, 25, 0, -25, -50, -100, -150, -300, -500, -800, -1000] 18 | # Number of total rays produced by the focal spot 19 | nRays = 100000 20 | # Production of rays from a focal spot with a radius determined by 21 | # focalRadius 22 | inputRays = RandomUniformRays(yMax=focalRadius, yMin=-focalRadius, maxCount=nRays) 23 | # Focal length of the objective 24 | objFocalLength = 5 25 | 26 | def exampleCode(): 27 | 28 | for pinhole in pinholeModifier: 29 | print("\nComputing transmission for pinhole size {0:0.1f}".format(pinhole)) 30 | 31 | efficiencyValues = [] 32 | for z in positions: 33 | print(".",end='') 34 | newPosition = 5 + (z * 0.000001) 35 | efficiency = illuminationPath(pinholeFactor=pinhole, focalSpotPosition=newPosition) 36 | efficiencyValues.append(efficiency) 37 | pinholeModifier[pinhole] = efficiencyValues 38 | 39 | plt.plot(positions, pinholeModifier[1 / 3], 'k:', label='Small pinhole', linestyle='dashed') 40 | plt.plot(positions, pinholeModifier[1], 'k-', label='Ideal pinhole') 41 | plt.plot(positions, pinholeModifier[3], 'k--', label='Large pinhole', linestyle='dotted') 42 | plt.ylabel('Transmission efficiency') 43 | plt.xlabel('Position of the focal spot (nm)') 44 | plt.legend() 45 | plt.show() 46 | 47 | def path(focalSpotPosition=objFocalLength): 48 | illumination = ImagingPath() 49 | illumination.append(Space(d=focalSpotPosition)) 50 | illumination.append(Lens(f=objFocalLength)) 51 | illumination.append(Space(d=105)) 52 | illumination.append(Lens(f=100)) 53 | illumination.append(Space(d=100)) 54 | illumination.append(System4f(f1=100, f2=75)) 55 | illumination.append(System4f(f1=40, f2=50)) # Path finishes at the pinhole position 56 | 57 | return illumination 58 | 59 | def optimalPinholeSize(): 60 | """ 61 | Finds the magnification of the optical path and use it to find the optimal pinhole size when the focal spot is at one 62 | focal length distance of the objective. 63 | 64 | Return 65 | ------- 66 | pinholeIdeal : Float 67 | Returns the optimal pinhole size 68 | """ 69 | 70 | # Dictionnary of the position and magnification of all conjugate planes of the focal spot. 71 | planes = path().intermediateConjugates() 72 | # The last conjugate plane is the pinhole. The magnification of this position is saved in mag. 73 | mag = planes[-1][1] 74 | # Calculates the pinhole size that fits perfectly the focal spot diameter. 75 | pinholeIdeal = abs(mag * (focalRadius * 2)) 76 | 77 | return pinholeIdeal 78 | 79 | 80 | def illuminationPath(pinholeFactor=None, focalSpotPosition=None): 81 | """ 82 | Determines the amount of rays emitted from the object that are detected at the pinhole plane. 83 | 84 | Parameter 85 | --------- 86 | pinholeFactor : Float 87 | Factor changing the pinhole size according to the ideal pinhole size. 88 | 89 | focalSpotPosition : float 90 | Position of the focal spot according to the objective (first lens) 91 | 92 | Returns 93 | ------- 94 | illumination : object of ImagingPath class. 95 | Returns the illumination path 96 | """ 97 | 98 | illumination = path(focalSpotPosition) 99 | 100 | pinholeSize = optimalPinholeSize() * pinholeFactor 101 | illumination.append(Aperture(diameter=pinholeSize)) 102 | 103 | # Counts how many rays make it through the pinhole 104 | outputRays = illumination.traceManyThroughInParallel(inputRays, progress=False) 105 | 106 | return outputRays.count / inputRays.count 107 | 108 | if __name__ == "__main__": 109 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/envexamples.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import matplotlib.pyplot as plt 4 | 5 | """ This file is needed so the examples in the `examples` directory can be run direclty from there with 6 | the current version of the module """ 7 | 8 | # Append module root directory to sys.path 9 | sys.path.insert(0, os.path.dirname( os.path.dirname( os.path.dirname( os.path.abspath(__file__) ) )) ) 10 | 11 | try: 12 | import raytracing 13 | except Exception as err: 14 | print("Unable to import : {0} {1}".format(err, sys.path)) 15 | 16 | # Some decent parameters for plots 17 | # See https://matplotlib.org/api/font_manager_api.html#matplotlib.font_manager.FontProperties.set_size 18 | params = {'legend.fontsize': 'x-large', 19 | 'figure.figsize': (10, 7), 20 | 'axes.labelsize': 'x-large', 21 | 'axes.titlesize':'x-large', 22 | 'xtick.labelsize':'x-large', 23 | 'ytick.labelsize':'x-large', 24 | 'font.family':'helvetica'} 25 | plt.rcParams.update(params) 26 | 27 | -------------------------------------------------------------------------------- /raytracing/examples/ex01.py: -------------------------------------------------------------------------------- 1 | TITLE = "A single lens f = 50 mm, infinite diameter" 2 | DESCRIPTION = """ Here we have a single lens of focal length f=50 mm. It has 3 | an infinite diameter. An default object (in blue) is positionned at 100 mm in 4 | front of the lens. The image conjugate (in red) is seen 100 mm after the lens, 5 | as expected. """ 6 | 7 | from raytracing import * 8 | 9 | def exampleCode(comments=None): 10 | path = ImagingPath() 11 | path.label = TITLE 12 | path.append(Space(d=100)) 13 | path.append(Lens(f=50)) 14 | path.append(Space(d=100)) 15 | path.display(comments=comments) 16 | 17 | if __name__ == "__main__": 18 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex02.py: -------------------------------------------------------------------------------- 1 | TITLE = "Two lenses, infinite diameters" 2 | DESCRIPTION = """ Here we have two thin lenses with infinite diameters. They 3 | are set up in a 4f configuration (telescope), since they are separated by the 4 | sum of their focal lengths. All elements are added to an ImagingPath in the 5 | order they are traversed. An ImagingPath assumes an object (in blue) at the 6 | front edge of the path. You can see the image conjugate (in red) at the focal 7 | plane of the second lens. """ 8 | 9 | from raytracing import * 10 | 11 | def exampleCode(comments=None): 12 | path = ImagingPath() 13 | path.label = TITLE 14 | path.append(Space(d=50)) 15 | path.append(Lens(f=50)) 16 | path.append(Space(d=200)) 17 | path.append(Lens(f=50)) 18 | path.append(Space(d=100)) 19 | path.display(comments=comments) 20 | 21 | if __name__ == "__main__": 22 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex03.py: -------------------------------------------------------------------------------- 1 | TITLE = "Finite-diameter lens" 2 | DESCRIPTION = """ An object at z=0 (front edge) is always present in 3 | ImagingPath. Notice the aperture stop (AS) identified at the lens which blocks 4 | the cone of light from the point of the object on the axis. There is no field 5 | stop to restrict the field of view, which is why we must use the default 6 | object and why we cannot restrict the display to the field of view. 7 | Notice the presence of an Aperture Stop (AS) but no Field Stop (FS).""" 8 | 9 | from raytracing import * 10 | 11 | def exampleCode(comments=None): 12 | path = ImagingPath() 13 | path.label = TITLE 14 | path.append(Space(d=100)) 15 | path.append(Lens(f=50, diameter=25)) 16 | path.append(Space(d=150)) 17 | path.display(comments=comments) 18 | 19 | if __name__ == "__main__": 20 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex04.py: -------------------------------------------------------------------------------- 1 | TITLE = "Aperture behind lens acting as Field Stop" 2 | DESCRIPTION = """ An object at z=0 (front edge of ImagingPath) is used with 3 | default properties. Notice the aperture stop (AS) identified at the lens which 4 | blocks the cone of light. The second aperture, after the lens, is the Field Stop 5 | (FS) and limits the field of view.""" 6 | 7 | from raytracing import * 8 | 9 | def exampleCode(comments=None): 10 | path = ImagingPath() 11 | path.label = TITLE 12 | path.append(Space(d=100)) 13 | path.append(Lens(f=50, diameter=30)) 14 | path.append(Space(d=30)) 15 | path.append(Aperture(diameter=30)) 16 | path.append(Space(d=170)) 17 | path.display(comments=comments) 18 | 19 | if __name__ == "__main__": 20 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex05.py: -------------------------------------------------------------------------------- 1 | TITLE = "Simple microscope system" 2 | DESCRIPTION = """ 3 | This is an extremely simple microscope with ideal lenses: the objective lens 4 | (labelled 'Obj') has a focal length of 4 mm and is positionned 184 mm from the 5 | tube lens (f=180 mm). You can zoom using the mouse to inspect the object. 6 | You can see the field of view (hollow blue arrow) and the object (filled blue 7 | arrow) at the focal plan of the objective. 8 | """ 9 | 10 | from raytracing import * 11 | 12 | def exampleCode(comments=None): 13 | path = ImagingPath() 14 | path.label = TITLE 15 | path.append(Space(d=4)) 16 | path.append(Lens(f=4, diameter=8, label='Obj')) 17 | path.append(Space(d=4 + 180)) 18 | path.append(Lens(f=180, diameter=50, label='Tube Lens')) 19 | path.append(Space(d=180)) 20 | path.display(ObjectRays(diameter=1, halfAngle=0.5),comments=comments) 21 | 22 | if __name__ == "__main__": 23 | exampleCode() 24 | -------------------------------------------------------------------------------- /raytracing/examples/ex06.py: -------------------------------------------------------------------------------- 1 | TITLE = "Kohler illumination" 2 | DESCRIPTION = """ Kohler illumination is a strategy to obtain the most uniform 3 | illumination at the sample. Instead of imaging the lamp filament on the 4 | sample, one images the lamp at the back of the objective, which will then 5 | project the Fourier transform of the illumination pattern at the object plane. 6 | Notice that the object (the lamp) has an image at the back focal plane of the 7 | objective. """ 8 | 9 | from raytracing import * 10 | 11 | def exampleCode(comments=DESCRIPTION): 12 | fobj, dObj, f2, d2, f3, d3 = 9, 20, 200, 50, 100, 50 13 | 14 | path = OpticalPath() 15 | path.append(Space(d=f3)) 16 | path.append(Lens(f=f3, diameter=d3)) 17 | path.append(Space(d=f3)) 18 | path.append(Space(d=f2)) 19 | path.append(Lens(f=f2, diameter=d2)) 20 | path.append(Space(d=f2)) 21 | path.append(Space(d=fobj)) 22 | path.append(Lens(f=fobj, diameter=dObj)) 23 | path.append(Space(d=fobj)) 24 | path.label = TITLE 25 | path.display(comments=comments) 26 | 27 | if __name__ == "__main__": 28 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex07.py: -------------------------------------------------------------------------------- 1 | TITLE = "Focussing through a dielectric slab" 2 | DESCRIPTION = """ When light focusses through a dielectric material, it is 3 | refracted. The effect on the position of the image can be see here: the first 4 | red arrow at z=200 mm arrow is the image that would have been obtained without 5 | the dielectric. However, the presence of this dielectric moves the final 6 | image to approximately 213 mm. Because all elements have infinite diameters, 7 | it is not possible to use the principal ray and the axial ray because they 8 | are not defined.""" 9 | 10 | from raytracing import * 11 | 12 | def exampleCode(comments=None): 13 | path = ImagingPath() 14 | path.label = TITLE 15 | path.append(Space(d=100)) 16 | path.append(Lens(f=50)) 17 | path.append(Space(d=30)) 18 | path.append(DielectricSlab(n=1.5, thickness=40)) 19 | path.append(Space(d=100)) 20 | path.display(comments=comments) 21 | 22 | if __name__ == "__main__": 23 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex08.py: -------------------------------------------------------------------------------- 1 | TITLE = "Virtual image at -f with object at f/2" 2 | DESCRIPTION = """ 3 | With an object midway between the focus and the lens, we obtain a virtual 4 | image at the front focal plane (d=-f to the left of the lens). 5 | The exact position can be obtained with path.forwardConjugate(), which 6 | will return the distance (from the end of the path) and the transfer matrix 7 | to that point. That transfer marix is necessarily an imaging matrix (B=0). 8 | """ 9 | 10 | from raytracing import * 11 | 12 | def exampleCode(comments=None): 13 | path = ImagingPath() 14 | path.label = TITLE 15 | path.append(Space(d=25)) 16 | path.append(Lens(f=50)) 17 | path.append(Space(d=50)) 18 | distance, transferMatrix = path.forwardConjugate() 19 | path.display(comments=comments) 20 | 21 | if __name__ == "__main__": 22 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex09.py: -------------------------------------------------------------------------------- 1 | TITLE = "Infinite telecentric 4f telescope" 2 | DESCRIPTION = """ Two lenses separated by the sum of their focal lengths is a 3 | telecentric telescope, or a 4f-system. If we set up the path from the focal 4 | plane of the first lens to the focal plane of the second lens (as is done 5 | here), then the system has a transfer matrix that is "imaging", with B=0. This 6 | can be checked with path.isImaging. 7 | 8 | Raytracing defines System4f() to conveniently put two lenses 9 | in this configuration: System4f(lens1, lens2). 10 | """ 11 | 12 | from raytracing import * 13 | 14 | def exampleCode(comments=None): 15 | path = ImagingPath() 16 | path.label = TITLE 17 | path.append(Space(d=50)) 18 | path.append(Lens(f=50)) 19 | path.append(Space(d=100)) 20 | path.append(Lens(f=50)) 21 | path.append(Space(d=50)) 22 | print("Transfer matrix of system is imaging (i.e. B=0): {0}".format(path.isImaging)) 23 | path.display(comments=comments) 24 | 25 | if __name__ == "__main__": 26 | exampleCode() 27 | -------------------------------------------------------------------------------- /raytracing/examples/ex10.py: -------------------------------------------------------------------------------- 1 | TITLE = "Retrofocus $f_e$={0:.1f} cm, and BFL={1:.1f}" 2 | DESCRIPTION = """ A retrofocus is a system of lenses organized in such a way 3 | that the effective focal length is shorter than the back focal length (i.e. 4 | the distance between the surface of the lens and the focal spot). It consists 5 | of a diverging lens followed by a converging lens. This is used to obtain a 6 | short focal length in situations where distances are constrained. """ 7 | 8 | from raytracing import * 9 | 10 | def exampleCode(comments=None): 11 | path = ImagingPath() 12 | path.append(Space(d=20)) 13 | path.append(Lens(f=-10, label='Div')) 14 | path.append(Space(d=7)) 15 | path.append(Lens(f=10, label='Foc')) 16 | path.append(Space(d=40)) 17 | (focal, focal) = path.effectiveFocalLengths() 18 | bfl = path.backFocalLength() 19 | path.label = TITLE.format(focal, bfl) 20 | path.display(comments=comments) 21 | 22 | if __name__ == "__main__": 23 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex11.py: -------------------------------------------------------------------------------- 1 | TITLE = "Thick diverging lens computed from the Lensmaker equation" 2 | DESCRIPTION = """ Raytracing offers a simple "thick lens" that has a non-null 3 | thickness. You need to provide R1 and R2 as well as the index and the 4 | thickness of the lens. It computes a single transfer matrix based on the 5 | application of a dielectric, space and dielectric transfer matrix. Notice 6 | that, in line with the paraxial approximation, the vertex is where the 7 | curvature of the rays occur, even if in this case the curvature brings the 8 | surface away from the vertex. Blocking of rays inside the lens is not 9 | calculated: you would need to build a MatrixGroup with all elements to do so 10 | (and this is what CompoundLens and AchromaticDoublets do).""" 11 | 12 | from raytracing import * 13 | 14 | def exampleCode(comments=None): 15 | path = ImagingPath() 16 | path.label = TITLE 17 | path.append(Space(d=50)) 18 | path.append(ThickLens(R1=-20, R2=20, n=1.55, thickness=10, diameter=25, label='Lens')) 19 | path.append(Space(d=50)) 20 | path.displayWithObject(diameter=20, comments=comments) 21 | 22 | if __name__ == "__main__": 23 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex12.py: -------------------------------------------------------------------------------- 1 | TITLE = "Thick diverging lens built from individual elements" 2 | DESCRIPTION = """ If you build a lens from individual elements 3 | (DielectricInterface, Space and DielectrcInterface), then the rays inside the 4 | lens may be blocked from the finite diameter (in this case, Space() has a 5 | finite diameter of 25 mm). The drawing on the graph is not as elegant, but 6 | the physics is more correct. Notice the upward rays from the green point is 7 | actually blocked inside. However, notice that again the rays are refracting 8 | at the vertex, in accord with the paraxial approximation. """ 9 | 10 | from raytracing import * 11 | 12 | def exampleCode(comments=None): 13 | # Demo #12: Thick diverging lens built from individual elements 14 | path = ImagingPath() 15 | path.label = TITLE 16 | path.append(Space(d=50)) 17 | path.append(DielectricInterface(R=-20, n1=1.0, n2=1.55, diameter=25, label='Front')) 18 | path.append(Space(d=10, diameter=25, label='Lens')) 19 | path.append(DielectricInterface(R=20, n1=1.55, n2=1.0, diameter=25, label='Back')) 20 | path.append(Space(d=50)) 21 | path.displayWithObject(diameter=20, comments=comments) 22 | 23 | 24 | if __name__ == "__main__": 25 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex13.py: -------------------------------------------------------------------------------- 1 | TITLE = "Obtain the forward and backward conjugates" 2 | DESCRIPTION = """ This example does not show a plot, it calculates the 3 | position of the image. forwardConjugate() finds the position and transfer 4 | matrix assuming an object at the front of the matrix/element. The distance is 5 | given from the end of the element. This is typically when we wish to obtain 6 | the image position and the transfer matrix to the image. With 7 | backwardConjugate(), we can also assume an image at the end of the 8 | matrix/element, and calculate where the object is to obtain an image at the 9 | poin (the distance is given from the front of the element). """ 10 | 11 | from raytracing import * 12 | 13 | def exampleCode(comments=None): 14 | M1 = Space(d=10) 15 | M2 = Lens(f=5) 16 | M3 = M2 * M1 17 | print("Image is at distance and transfer matrix: {0} ".format(M3.forwardConjugate())) 18 | print("Object is at distance and transfer matrix: {0} ".format(M3.backwardConjugate())) 19 | 20 | if __name__ == "__main__": 21 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex14.py: -------------------------------------------------------------------------------- 1 | TITLE = "Generic objectives" 2 | 3 | DESCRIPTION = """ It is possible to define microscopy Objectives that will 4 | behave similarly to real objectives (with a finite thickness, a back aperture, 5 | etc...). Using the specifications of an objective, one can use the Objective 6 | class to model essentially any objective. Real objectives from manufacturers 7 | (Olympus, Nikon) are included in the module.""" 8 | 9 | from raytracing import * 10 | 11 | def exampleCode(comments=None): 12 | obj = Objective(f=10, NA=0.8, focusToFocusLength=60, backAperture=18, workingDistance=2, 13 | magnification=40, fieldNumber=1.4, label="Objective") 14 | print("Focal distances: ", obj.focalDistances()) 15 | print("Position of PP1 and PP2: ", obj.principalPlanePositions(z=0)) 16 | print("Focal spots positions: ", obj.focusPositions(z=0)) 17 | print("Distance between entrance and exit planes: ", obj.L) 18 | 19 | path = ImagingPath() 20 | path.label = "Path with generic objective" 21 | path.append(Space(180)) 22 | path.append(obj) 23 | path.append(Space(10)) 24 | 25 | path.displayWithObject(diameter=20, fanAngle=0.005, comments=comments) 26 | 27 | if __name__ == "__main__": 28 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex15.py: -------------------------------------------------------------------------------- 1 | TITLE = "Model Olympus objective LUMPlanFL40X" 2 | DESCRIPTION = """ Using the Objective class of Raytracing, we have used the 3 | specifications of the LUMPlanFL40X objective to model it for use. It can be 4 | used in any system, and it can be flipped with the flip() command if needed. 5 | """ 6 | 7 | 8 | def exampleCode(comments=None): 9 | from raytracing import ImagingPath, Space, olympus 10 | path = ImagingPath() 11 | path.label = TITLE 12 | path.append(Space(180)) 13 | path.append(olympus.LUMPlanFL40X()) 14 | path.append(Space(10)) 15 | path.displayWithObject(diameter=10, fanAngle=0.005, comments=comments) 16 | 17 | if __name__ == "__main__": 18 | import envexamples 19 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex16.py: -------------------------------------------------------------------------------- 1 | TITLE = "Commercial doublets from Thorlabs and Edmund" 2 | DESCRIPTION = """ Several achromatic doublets from Thorlabs and Edmund Optics 3 | have been included (the visible series AC254-xxx-A from thorlabs for 4 | instance). They have been modelled according to the specifications from both 5 | manufacturers using the complete dispersion curve for glasses through a 6 | Material class and the curvatures of the surfaces. It is possible to show the 7 | focal shifts from chromatic aberrations. A Zemax file reader (ZMXReader) also 8 | exists to read other lenses. """ 9 | 10 | def exampleCode(comments=None): 11 | from raytracing import thorlabs, eo 12 | 13 | thorlabs.AC254_050_A().display() 14 | thorlabs.AC254_100_A().showChromaticAberrations() 15 | eo.PN_33_921().display() 16 | 17 | if __name__ == "__main__": 18 | import envexamples 19 | exampleCode() 20 | 21 | -------------------------------------------------------------------------------- /raytracing/examples/ex17.py: -------------------------------------------------------------------------------- 1 | TITLE = "An optical system with vendor lenses" 2 | DESCRIPTION = """ 3 | All vendor lenses could be used just like any other elements. Remember to 4 | check backFocalLength() and effectiveFocalLengths() to understand that the focal 5 | point is not "f_e" after the lens but rather "BFL" after the lens. 6 | """ 7 | 8 | from raytracing import * 9 | 10 | def exampleCode(comments=None): 11 | path = ImagingPath() 12 | path.label = TITLE 13 | path.append(Space(d=50)) 14 | path.append(thorlabs.AC254_050_A()) 15 | path.append(Space(d=50)) 16 | path.append(thorlabs.AC254_050_A()) 17 | path.append(Space(d=150)) 18 | path.append(eo.PN_33_921()) 19 | path.append(Space(d=50)) 20 | path.append(eo.PN_88_593()) 21 | path.append(Space(180)) 22 | path.append(olympus.LUMPlanFL40X()) 23 | path.append(Space(10)) 24 | path.display(comments=comments) 25 | 26 | if __name__ == "__main__": 27 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex18.py: -------------------------------------------------------------------------------- 1 | TITLE = "Laser beam and vendor lenses" 2 | DESCRIPTION = """ 3 | It is possible to propagate gaussian beams using a LaserPath instead of an imaging 4 | path. The formalism makes use of the same matrices, but the GaussianBeam is 5 | different from a ray: it is a complex radius of curvature (q), but all the complexity 6 | is hidden in the GaussianBeam class (although you can access q, w, zo, etc..). 7 | Note that any diffraction of the beam from edges of element is not considered 8 | because the formalism does not allow it: a gaussian beam remains a gaussian beam 9 | and therefore will not be clipped by lenses. 10 | """ 11 | 12 | from raytracing import * 13 | 14 | def exampleCode(comments=None): 15 | # Demo #18: Laser beam and vendor lenses 16 | path = LaserPath() 17 | path.label = TITLE 18 | path.append(Space(d=50)) 19 | path.append(thorlabs.AC254_050_A()) 20 | path.append(Space(d=50)) 21 | path.append(thorlabs.AC254_050_A()) 22 | path.append(Space(d=150)) 23 | path.append(eo.PN_33_921()) 24 | path.append(Space(d=50)) 25 | path.append(eo.PN_88_593()) 26 | path.append(Space(d=180)) 27 | path.append(olympus.LUMPlanFL40X()) 28 | path.append(Space(d=10)) 29 | path.display(beams=[GaussianBeam(w=0.001)], comments=comments) 30 | 31 | if __name__ == "__main__": 32 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/ex19.py: -------------------------------------------------------------------------------- 1 | TITLE = "Cavity round trip and calculated laser modes" 2 | DESCRIPTION = """ 3 | Although less developed in the raytracing module, it is also possible to calculate 4 | eigenmodes of laser cavities. 5 | """ 6 | 7 | from raytracing import * 8 | 9 | def exampleCode(comments=None): 10 | cavity = LaserCavity(label="") 11 | cavity.append(Space(d=160)) 12 | cavity.append(DielectricSlab(thickness=100, n=1.8)) 13 | cavity.append(Space(d=160)) 14 | cavity.append(CurvedMirror(R=-400)) 15 | cavity.append(Space(d=160)) 16 | cavity.append(DielectricSlab(thickness=100, n=1.8)) 17 | cavity.append(Space(d=160)) 18 | 19 | # Calculate all self-replicating modes (i.e. eigenmodes) 20 | (q1, q2) = cavity.eigenModes() 21 | print(q1,q2) 22 | 23 | # Obtain all physical modes (i.e. only finite eigenmodes) 24 | qs = cavity.laserModes() 25 | for q in qs: 26 | print(q) 27 | 28 | cavity.display(comments=comments) 29 | 30 | if __name__ == "__main__": 31 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/fig1-matrices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/fig1-matrices.png -------------------------------------------------------------------------------- /raytracing/examples/fig2-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/fig2-properties.png -------------------------------------------------------------------------------- /raytracing/examples/fig3-apertures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/fig3-apertures.png -------------------------------------------------------------------------------- /raytracing/examples/fig4-overviewFigure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/fig4-overviewFigure.png -------------------------------------------------------------------------------- /raytracing/examples/fig5-LSM_reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/fig5-LSM_reference.png -------------------------------------------------------------------------------- /raytracing/examples/fig7-kohler.py: -------------------------------------------------------------------------------- 1 | TITLE = "Kohler and critical (i.e. non-Kohler) illumination" 2 | DESCRIPTION = ''' This code compares the same system with and without Kohler illumination. 3 | The overlay of conjugate planes can determine indeed if the light source is 4 | imaged with the sample or not, significantly reducing the quality of the 5 | images. ''' 6 | 7 | from raytracing import * 8 | 9 | def exampleCode(comments=None): 10 | illumination = ImagingPath() 11 | illumination.design(fontScale=1.5) 12 | illumination.append(Space(d=20)) 13 | illumination.append(Lens(f=10, diameter=25.4, label="Collector")) 14 | illumination.append(Space(d=30)) 15 | illumination.append(Aperture(diameter=2, label="Field diaphragm")) 16 | illumination.append(Space(d=10+30)) 17 | illumination.append(Lens(f=30, diameter=25.4, label="Condenser")) 18 | illumination.append(Space(d=30+30)) 19 | illumination.append(Lens(f=30, diameter=25.4, label="Objective")) 20 | illumination.append(Space(d=30+30)) 21 | illumination.append(Lens(f=30, diameter=25.4, label="Tube")) 22 | illumination.append(Space(d=30+30)) 23 | illumination.append(Lens(f=30, diameter=25.4, label="Eyepiece")) 24 | illumination.append(Space(d=30+2)) 25 | illumination.append(Lens(f=2, diameter=10, label="Eye Entrance")) 26 | illumination.append(Space(d=2)) 27 | illumination.display(interactive=False, raysList=[ 28 | LampRays(diameter=0.1, NA=0.5, N=2, T=6, z=6.6666666, rayColors='r', label="Source"), 29 | ObjectRays(diameter=2, halfAngle=0.1, H=2, T=2, z=120, rayColors='g', color='g', label="Sample")], removeBlocked=False) 30 | 31 | if __name__ == "__main__": 32 | import envexamples 33 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/fig8-widefield.py: -------------------------------------------------------------------------------- 1 | TITLE = "The effect of lens diameters on collection efficiency" 2 | DESCRIPTION = """ 3 | In any imaging system, lens diameters are of great importance as 4 | they dictate the position of the Aperture Stop(AS) and Field Stop(FS). 5 | We recall that the AS controls the light acceptance and that a 6 | poorly placed FS causes vignetting. 7 | 8 | The following code shows a simple imaging system with three different 9 | paths containing different lens diameter. 10 | 11 | (a). The first one shows that if both lenses are too small, 12 | the AS is the first lens and the FS is the second lens. 13 | We get vignetting on the final image, since the FS is poorly 14 | placed at a lens instead of the detector's camera. 15 | (b). The second one shows that the second lens is smaller 16 | than the first one, so the FS is at the camera but 17 | AS is on the second lens, which is suboptimal. 18 | (c). The last one shows that both lenses are big enough to make the first 19 | lens the AS and the detector's camera the FS. 20 | """ 21 | 22 | from raytracing import * 23 | import gc 24 | 25 | # Defines the path. a and b are the diameter of the lenses. 26 | def imagingPathPreset(a=10, b=10, title=""): 27 | 28 | path = ImagingPath() 29 | path.design(fontScale=1.7) 30 | path.label=title 31 | path.append(System4f(f1=50, diameter1=a, f2=50, diameter2=b)) 32 | path.append(Aperture(diameter=10, label='Camera')) 33 | 34 | return path 35 | 36 | def exampleCode(comments=None): 37 | # Input from the expected field of view 38 | inputRays = RandomUniformRays(yMax = 5, 39 | yMin = -5, 40 | thetaMin = -0.5, 41 | thetaMax = +0.5, 42 | maxCount=1000000) 43 | 44 | # Three paths with different sets of lens diameter. 45 | path1 = imagingPathPreset(a=15, b=15, title="Vignetting with FS poorly placed because of second lens diameter") 46 | outputRays = path1.traceManyThrough(inputRays) 47 | efficiency = 100*outputRays.count/inputRays.count 48 | path1.display(ObjectRays(diameter=4,H=3,T=5, halfAngle=0.15), removeBlocked=False) 49 | outputRays.display("Output profile with vignetting {0:.0f}% efficiency".format(efficiency), showTheta=False) 50 | path1.reportEfficiency() 51 | 52 | gc.collect() 53 | 54 | path2 = imagingPathPreset(a=40, b=15, title="Suboptimal AS at second lens, but without vignetting") 55 | outputRays = path2.traceManyThrough(inputRays) 56 | efficiency = 100*outputRays.count/inputRays.count 57 | path2.display(ObjectRays(diameter=4,H=3,T=5, halfAngle=0.15), removeBlocked=False) 58 | outputRays.display("Output profile {0:.0f}% efficiency".format(efficiency), showTheta=False) 59 | path2.reportEfficiency() 60 | 61 | gc.collect() 62 | 63 | path3 = imagingPathPreset(a=25, b=50, title="Better AS at first lens and FS at Camera") 64 | outputRays = path3.traceManyThrough(inputRays) 65 | efficiency = 100*outputRays.count/inputRays.count 66 | path3.display(ObjectRays(diameter=4,H=3,T=5, halfAngle=0.15), removeBlocked=False) 67 | outputRays.display("Output profile {0:.0f}% efficiency".format(efficiency), showTheta=False) 68 | path3.reportEfficiency() 69 | 70 | if __name__ == "__main__": 71 | import envexamples 72 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/illuminator.py: -------------------------------------------------------------------------------- 1 | TITLE = "Kohler illumination" 2 | DESCRIPTION = """ 3 | """ 4 | 5 | import envexamples 6 | from raytracing import * 7 | 8 | def exampleCode(): 9 | path = ImagingPath() 10 | path.name = "Kohler illumination with 1 cm wide lamp and 0.5 NA" 11 | path.append(Space(d=40)) 12 | path.append(Lens(f=40,diameter=25, label='Collector')) 13 | path.append(Space(d=40+25)) 14 | path.append(Lens(f=25, diameter=75, label='Condenser')) 15 | path.append(Space(d=25)) 16 | path.append(Space(d=9)) 17 | path.append(Lens(f=9, diameter=8, label='Objective')) 18 | path.append(Space(d=9)) 19 | path.showLabels=True 20 | print(path.fieldStop()) 21 | print(path.fieldOfView()) 22 | path.display(ObjectRays(diameter=20, H=3, T=3, halfAngle=0.5), onlyPrincipalAndAxialRays=True, limitObjectToFieldOfView=True) 23 | #path.saveFigure("Illumination.png", onlyPrincipalAndAxialRays=True, limitObjectToFieldOfView=True) 24 | 25 | if __name__ == "__main__": 26 | exampleCode() 27 | -------------------------------------------------------------------------------- /raytracing/examples/invariant.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | from raytracing import * 3 | 4 | path = ImagingPath() 5 | path.name = "4f system, 1 cm object, small lenses" 6 | path.append(Space(d=5)) 7 | path.append(Lens(f=5, diameter=2.5)) 8 | path.append(Space(d=15)) 9 | path.append(Lens(f=10,diameter=2.5)) 10 | path.append(Space(d=10)) 11 | path.display() 12 | #path.saveFigure('object-smallLenses.png') 13 | 14 | path = ImagingPath() 15 | path.name = "4f system, 1 cm object, small and large lenses" 16 | path.append(Space(d=5)) 17 | path.append(Lens(f=5, diameter=2.5)) 18 | path.append(Space(d=15)) 19 | path.append(Lens(f=10,diameter=5)) 20 | path.append(Space(d=10)) 21 | path.display() 22 | #path.saveFigure('object-smallLargeLenses.png') 23 | 24 | path = ImagingPath() 25 | path.name = "4f system, calculated field of view, small lenses" 26 | path.append(Space(d=5)) 27 | path.append(Lens(f=5, diameter=2.5)) 28 | path.append(Space(d=15)) 29 | path.append(Lens(f=10,diameter=2.5)) 30 | path.append(Space(d=10)) 31 | path.display(onlyPrincipalAndAxialRays=True, limitObjectToFieldOfView=True) 32 | #path.saveFigure('fov-smallLenses.png', onlyChiefAndMarginalRays=True, limitObjectToFieldOfView=True) 33 | 34 | path = ImagingPath() 35 | path.name = "4f system, improved field of view, small and large lenses" 36 | path.append(Space(d=5)) 37 | path.append(Lens(f=5, diameter=2.5)) 38 | path.append(Space(d=15)) 39 | path.append(Lens(f=10,diameter=5.0)) 40 | path.append(Space(d=10)) 41 | path.display(onlyPrincipalAndAxialRays=True, limitObjectToFieldOfView=True) 42 | #path.saveFigure('fov-smallLargeLenses.png', onlyChiefAndMarginalRays=True, limitObjectToFieldOfView=True) 43 | 44 | path = ImagingPath() 45 | path.name = "4f systeme, no change in field of view with large first lens" 46 | path.append(Space(d=5)) 47 | path.append(Lens(f=5, diameter=5.0)) 48 | path.append(Space(d=15)) 49 | path.append(Lens(f=10,diameter=5.0)) 50 | path.append(Space(d=10)) 51 | path.display(onlyPrincipalAndAxialRays=True, limitObjectToFieldOfView=True) 52 | #path.saveFigure('fov-largeLenses.png', onlyChiefAndMarginalRays=True, limitObjectToFieldOfView=True) 53 | -------------------------------------------------------------------------------- /raytracing/examples/invariantAndEfficiency.py: -------------------------------------------------------------------------------- 1 | import envexamples # modifies path 2 | from raytracing import * 3 | 4 | """ 5 | The Lagrange invariant is a constant defining the collection efficiency of an optical system. The Lagrange 6 | invariant is calculated using the principal and axial rays, whether the optical invariant is calculated with 7 | another combination of rays. This code uses the optical invariant to characterize the ray transmission in a 8 | 4f system and shows that the optical invariant is greatly affected by the used optics. Indeed, changing the 9 | diameter of the first lens affects the number of detected rays at the imaged plane. 10 | """ 11 | 12 | path = ImagingPath() 13 | path.design(fontScale=1.7) 14 | path.append(System4f(f1=10, diameter1=25.4, f2=20, diameter2=25.4)) 15 | path.reportEfficiency() 16 | path.display(interactive=False) 17 | 18 | path2 = ImagingPath() 19 | path2.design(fontScale=1.5) 20 | path2.append(System4f(f1=10, diameter1=12.7, f2=20, diameter2=25.4)) 21 | path2.reportEfficiency() 22 | path2.display(interactive=False) -------------------------------------------------------------------------------- /raytracing/examples/kohlerAndInvariant.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | 3 | from raytracing import * 4 | 5 | ''' 6 | DESCRIPTION 7 | ''' 8 | 9 | #print("The Lagrange Invariant of this system is {}".format(illumination1.lagrangeInvariant())) 10 | 11 | def displayMultiple(paths: list, 12 | limitObjectToFieldOfView=False, 13 | onlyPrincipalAndAxialRays=False, 14 | removeBlockedRaysCompletely=False, 15 | comments=None): 16 | fig, axes = plt.subplots(figsize=(10, 7)) 17 | for path in paths: 18 | path.createRayTracePlot(axes=axes, limitObjectToFieldOfView=False, 19 | onlyPrincipalAndAxialRays=False, 20 | removeBlockedRaysCompletely=False) 21 | # can only def callbacks for only one path 22 | axes.callbacks.connect('ylim_changed', paths[0].updateDisplay) 23 | axes.set_ylim([-paths[0].displayRange(axes) / 2 * 1.5, paths[0].displayRange(axes) / 2 * 1.5]) 24 | paths[0]._showPlot() 25 | 26 | 27 | # Source with Kohler illumination 28 | illumination1 = ImagingPath() 29 | illumination1.fanNumber = 3 30 | illumination1.fanAngle = 0.1 31 | illumination1.design(rayColors = ["r","r","r"]) 32 | 33 | illumination1.append(Space(d=10)) 34 | illumination1.append(Lens(f=10, diameter=100, label="Collector")) 35 | illumination1.append(Space(d=10+30)) 36 | illumination1.append(Lens(f=30, diameter=100, label="Condenser")) 37 | illumination1.append(Space(d=30+30)) 38 | illumination1.append(Lens(f=30, diameter=100, label="Objective")) 39 | illumination1.append(Space(d=30+30)) 40 | illumination1.append(Lens(f=30, diameter=100, label="Tube")) 41 | illumination1.append(Space(d=30+30)) 42 | illumination1.append(Lens(f=30, diameter=100, label="Eyepiece")) 43 | illumination1.append(Space(d=30+2)) 44 | illumination1.append(Lens(f=2, diameter=10, label="Eye Entrance")) 45 | illumination1.append(Space(d=2)) 46 | illumination1.display() 47 | 48 | 49 | 50 | 51 | # Source without Kohler illumination 52 | illumination2 = ImagingPath() 53 | illumination2.fanNumber = 3 54 | illumination2.fanAngle = 0.1 55 | illumination2.objectPosition = 20 56 | illumination2.design(rayColors = ["b","b","b"]) 57 | 58 | illumination2.append(Space(d=30)) 59 | illumination2.append(Lens(f=30, diameter=100, label="Condenser")) 60 | illumination2.append(Space(d=30+30)) 61 | illumination2.append(Lens(f=30, diameter=100, label="Objective")) 62 | illumination2.append(Space(d=30+30)) 63 | illumination2.append(Lens(f=30, diameter=100, label="Tube")) 64 | illumination2.append(Space(d=30+30)) 65 | illumination2.append(Lens(f=30, diameter=100, label="Eyepiece")) 66 | illumination2.append(Space(d=30+2)) 67 | illumination2.append(Lens(f=2, diameter=10, label="Eye Entrance")) 68 | illumination2.append(Space(d=2)) 69 | illumination2.display() 70 | 71 | 72 | 73 | 74 | # Sample imaging path 75 | illumination3 = ImagingPath() 76 | illumination3.objectPosition = 80 77 | illumination3.objectHeight = 5 78 | illumination3.fanNumber = 1 79 | illumination3.fanAngle = 0 80 | illumination3.design(rayColors = ["g","g","g"]) 81 | 82 | 83 | illumination3.append(Space(d=10)) 84 | illumination3.append(Lens(f=10, diameter=100, label="Collector")) 85 | illumination3.append(Space(d=10+30)) 86 | illumination3.append(Lens(f=30, diameter=100, label="Condenser")) 87 | illumination3.append(Space(d=30+30)) 88 | illumination3.append(Lens(f=30, diameter=100, label="Objective")) 89 | illumination3.append(Space(d=30+30)) 90 | illumination3.append(Lens(f=30, diameter=100, label="Tube")) 91 | illumination3.append(Space(d=30+30)) 92 | illumination3.append(Lens(f=30, diameter=100, label="Eyepiece")) 93 | illumination3.append(Space(d=30+2)) 94 | illumination3.append(Lens(f=2, diameter=10, label="Eye Entrance")) 95 | illumination3.append(Space(d=2)) 96 | illumination3.display() 97 | 98 | #displayMultiple([illumination3, illumination1]) 99 | #displayMultiple([illumination3, illumination2]) 100 | -------------------------------------------------------------------------------- /raytracing/examples/laserScanningMicroscope.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | from raytracing import * 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | 6 | 7 | """ 8 | In a laser scanning system, the scanning components define 9 | the covered field-of-view (FOV) at the sample plane. Here, 10 | we show a one-dimensional example with a polygonal mirror 11 | of 36 facets that rotates rapidly to scan 12 | the beam along the horizontal direction. It produces a meachnical 13 | sweep of 10 degrees, or 0.1750 rad, between each facets. 14 | Therefore, the laser beam covers a total optical scan angle of 20 degrees. 15 | In the following example, the object is considered to be the 16 | laser beam at the polygonal mirror plane. 17 | The output profile shows on its x-axis the width of the FOV under the objective. 18 | """ 19 | 20 | # List of the scan angle of the ray making it throught the system. 21 | thetas = [] 22 | 23 | # List of 1 corresponding to the number of elements in heights 24 | # so that plt.plot() doesn't freak out. 25 | positions = [] 26 | 27 | # Radius of the laser beam at the scanning element. 28 | objectHalfHeight = 0.000250 29 | 30 | # Angle produced by the scanning element. 31 | scanAngle = 10*np.pi/180 32 | 33 | # Number of total rays considered in these calculations. 34 | nRays = 10000 35 | 36 | # Production of rays in the angle range of the scanning element. 37 | scanRays = UniformRays(yMax=0, thetaMax=scanAngle, M=1, N=nRays) 38 | 39 | class UISUPLAPO60XW(Objective): 40 | 41 | def __init__(self): 42 | super(UISUPLAPO60XW, self).__init__(f=180/60, 43 | NA=1.2, 44 | focusToFocusLength=40, 45 | backAperture=7, 46 | workingDistance=0.28, 47 | magnification=60, 48 | fieldNumber=22, 49 | label='UISUPLAPO60XW Objective') 50 | 51 | 52 | def illuminationPath(): 53 | 54 | illumination = ImagingPath() 55 | # The object in this situation is the laser beam at the scanning element. 56 | illumination.objectHeight = objectHalfHeight*2 57 | illumination.rayNumber = 3 58 | illumination.fanNumber = 3 59 | illumination.fanAngle = 0 60 | illumination.append(System4f(f1=40, f2=75, diameter1=24.5, diameter2=24.5)) 61 | illumination.append(System4f(f1=100, f2=100, diameter1=24.5, diameter2=24.5)) 62 | illumination.append(Space(d=180/40)) 63 | illumination.append(UISUPLAPO60XW()) 64 | illumination.append(Space(d=180/40)) 65 | 66 | return illumination 67 | 68 | 69 | path = illuminationPath() 70 | outputRays = path.traceManyThrough(scanRays) 71 | for i in range(len(outputRays)): 72 | thetas.append(scanRays[i].theta*180/np.pi) 73 | positions.append(outputRays[i].y*1000) 74 | 75 | scanRays.displayProgress() 76 | 77 | plt.plot(thetas,positions) 78 | plt.xlabel('Scan angle (degrees)') 79 | plt.ylabel('Scanning position of the focal spot (µm)') 80 | plt.show() 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /raytracing/examples/multiProcessingTracing.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | import time 3 | import numpy 4 | 5 | from raytracing import * 6 | from matplotlib import pyplot, patches 7 | 8 | fobj, backaperture = 5., 5. 9 | 10 | if __name__ == "__main__": 11 | 12 | reps = 1 13 | processes = None 14 | 15 | path = ImagingPath() 16 | 17 | # Objective 18 | space = fobj 19 | path.append(Space(d=space)) 20 | path.append(Lens(f=fobj, diameter=backaperture)) 21 | path.append(Space(d=fobj)) 22 | path.append(Space(d=space)) 23 | path.append(Lens(f=fobj, diameter=backaperture)) 24 | path.append(Space(d=fobj)) 25 | path.append(Space(d=space)) 26 | path.append(Lens(f=fobj, diameter=backaperture)) 27 | path.append(Space(d=fobj)) 28 | path.append(Space(d=space)) 29 | path.append(Lens(f=fobj, diameter=backaperture)) 30 | path.append(Space(d=fobj)) 31 | path.append(Space(d=space)) 32 | path.append(Lens(f=fobj, diameter=backaperture)) 33 | path.append(Space(d=fobj)) 34 | path.append(Space(d=space)) 35 | path.append(Lens(f=fobj, diameter=backaperture)) 36 | path.append(Space(d=fobj)) 37 | path.append(Space(d=space)) 38 | path.append(Lens(f=fobj)) 39 | path.append(Space(d=fobj)) 40 | 41 | test_functions = { 42 | "naive" : path.traceManyThrough, 43 | "chunks-multiprocessing" : path.traceManyThroughInParallel 44 | } 45 | 46 | fig, ax = pyplot.subplots() 47 | width = 1 / (len(test_functions) + 1) 48 | possible_rays = [1e+2, 1e+3, 1e+4,1e+5] 49 | for i, nRays in enumerate(possible_rays): 50 | nRays = int(nRays) 51 | inputRays = RandomUniformRays(yMax=0, maxCount=nRays) 52 | output = {} 53 | for name, func in test_functions.items(): 54 | times = [] 55 | for _ in range(reps): 56 | start = time.time() 57 | try: 58 | outputRays = func(inputRays, processes=processes) 59 | except TypeError: 60 | outputRays = func(inputRays) 61 | times.append(time.time() - start) 62 | output[name] = times 63 | xs = i + numpy.arange(len(test_functions)) / (len(test_functions) + 1) 64 | ax.bar(xs, [numpy.mean(values) for values in output.values()], 65 | yerr=[numpy.std(values) for values in output.values()], 66 | width=width, align="edge", color=["tab:blue", "tab:orange", "tab:green", "tab:red"]) 67 | ax.set( 68 | xticks = numpy.arange(len(possible_rays)) + len(test_functions) * width / 2, 69 | xticklabels = [f"{int(nRays)}" for nRays in possible_rays], 70 | xlabel = "Number of propagated rays", 71 | ylabel = "Calculation time (s)" 72 | ) 73 | ax.legend(handles = [patches.Patch(facecolor=color, label='Color Patch') for color in ["tab:blue", "tab:orange", "tab:green", "tab:red"]], 74 | labels = list(test_functions.keys())) 75 | fig.savefig("parallel_time_smallcounts.png", transparent=True, bbox_inches="tight") 76 | pyplot.show() 77 | 78 | ###################################################################### 79 | # Uncomment the following lines to calculate for more rays 80 | ###################################################################### 81 | 82 | # fig, ax = pyplot.subplots() 83 | # width = 1 / (len(test_functions) + 1) 84 | # possible_rays = [1e+5, 1e+6] 85 | # for i, nRays in enumerate(possible_rays): 86 | # nRays = int(nRays) 87 | # inputRays = RandomUniformRays(yMax=0, maxCount=nRays) 88 | # output = {} 89 | # for name, func in test_functions.items(): 90 | # times = [] 91 | # for _ in range(reps): 92 | # start = time.time() 93 | # outputRays = func(inputRays) 94 | # times.append(time.time() - start) 95 | # output[name] = times 96 | # xs = i + numpy.arange(len(test_functions)) / (len(test_functions) + 1) 97 | # ax.bar(xs, [numpy.mean(values) for values in output.values()], 98 | # yerr=[numpy.std(values) for values in output.values()], 99 | # width=width, align="edge", color=["tab:blue", "tab:orange", "tab:green", "tab:red"]) 100 | # ax.set( 101 | # xticks = numpy.arange(len(possible_rays)) + len(test_functions) * width / 2, 102 | # xticklabels = [f"{int(nRays)}" for nRays in possible_rays], 103 | # xlabel = "Number of propagated rays", 104 | # ylabel = "Calculation time (s)" 105 | # ) 106 | # ax.legend(handles = [patches.Patch(facecolor=color, label='Color Patch') for color in ["tab:blue", "tab:orange", "tab:green", "tab:red"]], 107 | # labels = list(test_functions.keys())) 108 | # fig.savefig("parallel_time.pdf", transparent=True, bbox_inches="tight") 109 | # pyplot.show() 110 | -------------------------------------------------------------------------------- /raytracing/examples/parallel_time_smallcounts.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/parallel_time_smallcounts.pdf -------------------------------------------------------------------------------- /raytracing/examples/parallel_time_smallcounts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/examples/parallel_time_smallcounts.png -------------------------------------------------------------------------------- /raytracing/examples/randomLambertianRays.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | from raytracing import * 3 | 4 | nRays = 1000000 # Increase for better resolution 5 | minHeight = -5 6 | maxHeight = 5 7 | 8 | # Lambertian: distribution proportional to cos theta 9 | inputRays = RandomLambertianRays(yMin = minHeight, 10 | yMax = maxHeight, 11 | maxCount = nRays) 12 | inputRays.display() 13 | -------------------------------------------------------------------------------- /raytracing/examples/randomUniformRays.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | from raytracing import * 3 | 4 | nRays = 1000000 # Increase for better resolution 5 | minHeight = -5 6 | maxHeight = 5 7 | minTheta = -0.5 # rad 8 | maxTheta = +0.5 9 | 10 | # define a list of rays with uniform distribution 11 | inputRays = RandomUniformRays(yMin = minHeight, 12 | yMax = maxHeight, 13 | maxCount = nRays, 14 | thetaMax = maxTheta, 15 | thetaMin = minTheta) 16 | inputRays.display() 17 | -------------------------------------------------------------------------------- /raytracing/examples/sourceCollection.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | from raytracing import * 3 | 4 | path = ImagingPath(label="Original setup") 5 | path.append(Space(d=100)) 6 | path.append(Lens(f=35,diameter=25)) 7 | path.append(Space(d=35)) 8 | path.append(Aperture(diameter=0.2)) 9 | path.displayWithObject(diameter=1, removeBlocked=False) 10 | path.reportEfficiency(objectDiameter=1, emissionHalfAngle=1.57) 11 | 12 | path2 = ImagingPath(label="Better system imaging the emission spot on detector") 13 | path2.append(Space(d=70)) 14 | path2.append(Lens(f=35,diameter=25)) 15 | path2.append(Space(d=70)) 16 | path2.append(Aperture(diameter=0.2)) 17 | path2.displayWithObject(diameter=1, removeBlocked=False) 18 | path2.reportEfficiency(objectDiameter=1, emissionHalfAngle=1.57) 19 | 20 | path3 = ImagingPath(label="Best system and larger NA imaging spot on detector") 21 | path3.objectHeight=1 22 | path3.append(System4f(f1=35, diameter1=25, f2=35, diameter2=35)) 23 | path3.append(Aperture(diameter=0.2)) 24 | path3.displayWithObject(diameter=1, removeBlocked=False) 25 | #path3.reportEfficiency(objectDiameter=1, emissionHalfAngle=1.57) 26 | -------------------------------------------------------------------------------- /raytracing/examples/template.py: -------------------------------------------------------------------------------- 1 | TITLE = "" 2 | DESCRIPTION = """ 3 | """ 4 | 5 | from raytracing import * 6 | 7 | def exampleCode(): 8 | pass 9 | # path.display(comments=DESCRIPTION) 10 | 11 | if __name__ == "__main__": 12 | exampleCode() -------------------------------------------------------------------------------- /raytracing/examples/twoPhotonDescannedDetector.py: -------------------------------------------------------------------------------- 1 | import envexamples 2 | from raytracing import * 3 | import matplotlib.pyplot as plt 4 | 5 | """ The emitted light in some optical systems, like two-photon descanned 6 | detector, is scattered in all directions. The size of the diffused light 7 | dictates which lenses and detector size to choose. Therefore, it is of great 8 | importance to find a well-sized detector that fits the optical system. """ 9 | 10 | # Defines the path. d1 and d2 are the diameter of the lenses, fl1 and fl2 are the focal lengths and d3 is the diameter of the aperture. 11 | def imagingPath(fl1=10, d1=10, fl2=10, d2=10, d3=10, title=""): 12 | 13 | path = ImagingPath() 14 | path.label=title 15 | path.append(System4f(f1=fl1, diameter1=d1, f2=fl2, diameter2=d2)) 16 | path.append(Aperture(diameter=d3, label='Detector')) 17 | 18 | return path 19 | 20 | def exampleCode(): 21 | nRays = 100000 22 | minHeight=-0.5 23 | maxHeight=0.5 24 | inputRays = RandomLambertianRays(yMax=maxHeight, yMin=minHeight, maxCount=nRays) 25 | 26 | # Three paths with different sets of lens and aperture. 27 | 28 | ### 29 | path1 = imagingPath(fl1=75, fl2=75, d1=50, d2=75, d3=0.5, title="") 30 | outputRays1 = path1.traceManyThrough(inputRays, progress=False) 31 | efficiency1 = 100*outputRays1.count/inputRays.count 32 | path1.display(limitObjectToFieldOfView=False, onlyPrincipalAndAxialRays=True) 33 | outputRays1.display("Output profile {0:.0f}% efficiency".format(efficiency1), showTheta=False) 34 | print(efficiency1) 35 | 36 | ### 37 | path2 = imagingPath(fl1=50, fl2=50, d1=25, d2=50, d3=0.5, title="") 38 | outputRays2 = path2.traceManyThrough(inputRays, progress=False) 39 | efficiency2 = 100*outputRays2.count/inputRays.count 40 | path2.display(limitObjectToFieldOfView=False, onlyPrincipalAndAxialRays=True) 41 | outputRays2.display("Output profile {0:.0f}% efficiency".format(efficiency2), showTheta=False) 42 | print(efficiency2) 43 | 44 | ### 45 | path3 = imagingPath(fl1=50, fl2=50, d1=25, d2=50, d3=1, title="") 46 | outputRays3 = path3.traceManyThrough(inputRays, progress=False) 47 | efficiency3 = 100*outputRays3.count/inputRays.count 48 | path3.display(limitObjectToFieldOfView=False, onlyPrincipalAndAxialRays=True) 49 | outputRays3.display("Output profile {0:.0f}% efficiency".format(efficiency3), showTheta=False) 50 | print(efficiency3) 51 | 52 | 53 | if __name__ == "__main__": 54 | exampleCode() -------------------------------------------------------------------------------- /raytracing/interface.py: -------------------------------------------------------------------------------- 1 | class Interface: 2 | def __init__(self, L=0.0, n=1.0): 3 | self.n = n 4 | self.L = L 5 | 6 | 7 | class SphericalInterface(Interface): 8 | def __init__(self, R: float, L=0.0, n=1.0): 9 | super(SphericalInterface, self).__init__(L=L, n=n) 10 | self.R = R 11 | 12 | 13 | class FlatInterface(SphericalInterface): 14 | def __init__(self, L=0.0, n=1.0): 15 | super(FlatInterface, self).__init__(R=float("+inf"), L=L, n=n) 16 | 17 | 18 | class ConicalInterface(Interface): 19 | def __init__(self, alpha: float, L=0.0, n=1.0): 20 | super(ConicalInterface, self).__init__(L=L, n=n) 21 | self.alpha = alpha 22 | -------------------------------------------------------------------------------- /raytracing/lasercavity.py: -------------------------------------------------------------------------------- 1 | from .matrixgroup import * 2 | from .imagingpath import * 3 | from .laserpath import * 4 | import warnings 5 | 6 | class LaserCavity(LaserPath): 7 | """A laser cavity (i.e. a resonator). The beam is considered to go 8 | through all elements from first to last, then again through the first element. 9 | 10 | 11 | Usage is to create the LaserCavity(), then append() elements 12 | and display(). You need to include the elements twice: forward then backward 13 | propagation. 14 | 15 | Parameters 16 | ---------- 17 | elements : list of elements 18 | A list of ABCD matrices in the laser cavity. Must include both forward and backward. 19 | You can use the copyReversedElements() function to help. Do not copy the last mirror twice. 20 | label : string 21 | the label for the laser cavity (Optional) 22 | 23 | Attributes 24 | ---------- 25 | showElementLabels : bool 26 | If True, the labels of the elements will be shown on display. (default=True) 27 | showPointsOfInterest : bool 28 | If True, the points of interest will be shown on display. (default=True) 29 | showPointsOfInterestLabels : bool 30 | If True, the labels of the points of interest will be shown on display. (default=True) 31 | showPlanesAcrossPointsOfInterest : bool 32 | If True, the planes across the points of interest will be shown on display. (default=True) 33 | 34 | See Also 35 | -------- 36 | raytracing.GaussianBeam 37 | 38 | Notes 39 | ----- 40 | 41 | """ 42 | 43 | def __init__(self, elements=None, label="Laser cavity"): 44 | super(LaserCavity, self).__init__(elements=elements, label=label) 45 | 46 | def appendElementsInReverse(self, skipFirstElement=False, skipLastElement=False): 47 | if skipFirstElement: 48 | firstElement = 1 49 | else: 50 | firstElement = 0 51 | 52 | if skipLastElement: 53 | forwardElements = self.elements[firstElement:-1] 54 | else: 55 | forwardElements = self.elements[firstElement:] 56 | 57 | for element in reversed(forwardElements): 58 | self.elements.append(element) 59 | 60 | 61 | def eigenModes(self): 62 | """ 63 | Returns the two complex radii that are identical after a 64 | round trip, assuming the matrix of the LaserCavity() is one 65 | round trip: you will need to duplicate elements in reverse 66 | and append them manually. 67 | 68 | Knowing that q = (Aq + B)/(Cq + d), we get 69 | Cq^2 + Dq = Aq + B, therefore: 70 | Cq^2 + (D-A)q - B = 0 71 | 72 | and q = - ((D-A) +- sqrt( (D-A)^2 - 4 C (-B)))/(2C) 73 | 74 | You will typically obtain two values, where only one is physical. 75 | There could be two modes in a laser with complex matrices (i.e. 76 | with gain), but this is not considered here. See "Lasers" by Siegman. 77 | """ 78 | if not self.hasPower: 79 | return None, None 80 | b = self.D - self.A 81 | sqrtDelta = cmath.sqrt(b * b - 4.0 * self.C * (-self.B)) 82 | 83 | q1 = (- b + sqrtDelta) / (2.0 * self.C) 84 | q2 = (- b - sqrtDelta) / (2.0 * self.C) 85 | return (GaussianBeam(q=q1), GaussianBeam(q=q2)) 86 | 87 | def laserModes(self): 88 | """ 89 | Returns the laser modes that are physical (finite) when 90 | calculating the eigenmodes. 91 | """ 92 | 93 | (q1, q2) = self.eigenModes() 94 | q = [] 95 | if q1 is not None and q1.isFinite: 96 | q.append(q1) 97 | 98 | if q2 is not None and q2.isFinite: 99 | q.append(q2) 100 | 101 | return q 102 | 103 | @property 104 | def isStable(self): 105 | beams = self.laserModes() 106 | if len(beams) > 0: 107 | return True 108 | 109 | return False 110 | 111 | def display(self, comments=None): # pragma: no cover 112 | """ Display the optical cavity and trace the laser beam. 113 | If comments are included they will be displayed on a 114 | graph in the bottom half of the plot. 115 | 116 | Parameters 117 | ---------- 118 | comments : string 119 | If comments are included they will be displayed on a 120 | graph in the bottom half of the plot. (default=None) 121 | 122 | """ 123 | 124 | if not self.isStable: 125 | warnings.warn("`LaserCavity.display()` called with an unstable cavity. No beam will be drawn.", BeginnerHint) 126 | super(LaserCavity, self).display(beams=[]) 127 | else: 128 | super(LaserCavity, self).display(beams=self.laserModes()) 129 | -------------------------------------------------------------------------------- /raytracing/laserpath.py: -------------------------------------------------------------------------------- 1 | from .matrixgroup import MatrixGroup 2 | from .figure import Figure 3 | 4 | 5 | class LaserPath(MatrixGroup): 6 | """The main class of the module for coherent 7 | laser beams: it is the combination of Matrix() or MatrixGroup() 8 | to be used as a laser path with a laser beam (GaussianBeam) 9 | at the entrance. 10 | 11 | Usage is to create the LaserPath(), then append() elements 12 | and display(). You may change the inputBeam to any GaussianBeam(), 13 | or provide one to display(beam=GaussianBeam()) 14 | 15 | Parameters 16 | ---------- 17 | elements : list of elements 18 | A list of ABCD matrices in the imaging path 19 | label : string 20 | the label for the imaging path (Optional) 21 | 22 | Attributes 23 | ---------- 24 | inputBeam : object of GaussianBeam class 25 | the input beam of the imaging path is defined using this parameter. 26 | showElementLabels : bool 27 | If True, the labels of the elements will be shown on display. (default=True) 28 | showPointsOfInterest : bool 29 | If True, the points of interest will be shown on display. (default=True) 30 | showPointsOfInterestLabels : bool 31 | If True, the labels of the points of interest will be shown on display. (default=True) 32 | showPlanesAcrossPointsOfInterest : bool 33 | If True, the planes across the points of interest will be shown on display. (default=True) 34 | 35 | See Also 36 | -------- 37 | raytracing.GaussianBeam 38 | 39 | Notes 40 | ----- 41 | Gaussian laser beams are not "blocked" by aperture. The formalism 42 | does not explicitly allow that. However, if it appears that a 43 | GaussianBeam() would be clipped by finite aperture, a property 44 | is set to indicate it, but it will propagate nevertheless 45 | and without diffraction due to that aperture. 46 | """ 47 | 48 | def __init__(self, elements=None, label=""): 49 | self.inputBeam = None 50 | self.showElementLabels = True 51 | self.showPointsOfInterest = True 52 | self.showPointsOfInterestLabels = True 53 | self.showPlanesAcrossPointsOfInterest = True 54 | 55 | self.figure = Figure(opticalPath=self) 56 | self.design = self.figure.design 57 | super(LaserPath, self).__init__(elements=elements, label=label) 58 | 59 | def display(self, beams=None, comments=None): # pragma: no cover 60 | """ Display the optical system and trace the laser beam. 61 | If comments are included they will be displayed on a 62 | graph in the bottom half of the plot. 63 | 64 | Parameters 65 | ---------- 66 | inputBeam : object of GaussianBeam class 67 | inputBeams : list of object of GaussianBeam class 68 | A list of Gaussian beams 69 | comments : string 70 | If comments are included they will be displayed on a graph in the bottom half of the plot. (default=None) 71 | 72 | """ 73 | if beams is None: 74 | beams = [self.inputBeam] 75 | 76 | self.figure.displayGaussianBeam(beams=beams, 77 | comments=comments, title=self.label, backend='matplotlib', display3D=False) 78 | -------------------------------------------------------------------------------- /raytracing/nikon.py: -------------------------------------------------------------------------------- 1 | from .specialtylenses import * 2 | 3 | class LWD16X(Objective): 4 | """ Nikon 16x immersion objective 5 | 6 | .. csv-table:: 7 | :header: Parameter, value 8 | 9 | "f", "200/16" 10 | "backAperture", "20" 11 | "Numerical Aperture (NA)", "1.56" 12 | "Working Distance (mm)", "3" 13 | 14 | Notes 15 | ----- 16 | Immersion not considered at this point. 17 | """ 18 | 19 | def __init__(self): 20 | super(LWD16X, self).__init__(f=200/16, 21 | NA=1.95*0.8, 22 | focusToFocusLength=75, 23 | backAperture=20, 24 | workingDistance=3, 25 | label='CFI75 LWD 16x W', 26 | url="https://www.thorlabs.com/thorproduct.cfm?partnumber=N16XLWD-PF") 27 | -------------------------------------------------------------------------------- /raytracing/preferences.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import platform 4 | 5 | class Preferences(dict): 6 | def __init__(self, *args, **kwargs): 7 | self.update(*args, **kwargs) 8 | 9 | prefFilename = "ca.dcclab.python.raytracing.json" 10 | prefDir = "." 11 | 12 | try: 13 | if platform.system() == 'Darwin': 14 | home = os.path.expanduser("~") 15 | prefDir = os.path.join(home, "Library","Preferences") 16 | elif platform.system() == 'Windows': 17 | from win32com.shell import shell, shellcon 18 | prefDir = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0) 19 | elif platform.system() == 'Linux': 20 | home = os.path.expanduser("~") 21 | prefDir = os.path.join(home, ".config") 22 | if not os.path.exists(prefDir): 23 | os.mkdir(prefDir) 24 | except Exception as err: 25 | prefDir = "." 26 | 27 | self.path = os.path.join(prefDir, prefFilename) 28 | self.readFromDisk() 29 | 30 | def update(self, *args, **kwargs): 31 | for k, v in dict(*args, **kwargs).items(): 32 | self[k] = v 33 | 34 | def __getitem__(self, key): 35 | self.readFromDisk() 36 | return super().__getitem__(key) 37 | 38 | def __setitem__(self, key, value): 39 | super().__setitem__(key, value) 40 | self.writeToDisk() 41 | 42 | def resetPreferences(self): 43 | try: 44 | os.remove(self.path) 45 | except Exception as err: 46 | pass 47 | self.clear() 48 | self.writeToDisk() 49 | 50 | def readFromDisk(self): 51 | if os.path.exists(self.path): 52 | with open(self.path, "r+") as prefFile: 53 | try: 54 | data = json.load(prefFile) 55 | except Exception as err: 56 | data = dict() 57 | self.update(data) 58 | else: 59 | self.clear() 60 | self.writeToDisk() 61 | 62 | def writeToDisk(self): 63 | with open(self.path, "w+") as prefFile: 64 | json.dump(self, prefFile) 65 | -------------------------------------------------------------------------------- /raytracing/specifications/AC254-100-A-Zemax(ZMX).zmx: -------------------------------------------------------------------------------- 1 | VERS 120928 0 30221 2 | MODE SEQ 3 | NAME AC254-100-A AC254-100-A POSITIVE VISIBLE ACHROMATS: Infinite 100 4 | NOTE 0 FOR INFORMATION ONLY, NOT FOR MANUFACTURING. ALL VALUES AND COATING(S) ARE TYPICAL AND THEORETICAL, ACTUAL PERFORMANCE MAY VARY. FOR FURTHER INFORMATION CONTACT CUSTOMER SERVICE AT TECHSUPPORT@THORL 5 | NOTE 0 ABS.COM 6 | PFIL 0 0 0 7 | UNIT MM X W X CM MR CPMM 8 | ENPD 2.286E+1 9 | ENVD 2.0E+1 1 0 10 | GFAC 0 0 11 | GCAT SCHOTT INFRARED MISC 12 | RAIM 0 0 1 1 0 0 0 0 0 13 | PUSH 0 0 0 0 0 0 14 | SDMA 0 1 0 15 | FTYP 0 0 1 3 0 0 0 16 | ROPD 2 17 | PICB 1 18 | XFLN 0 0 0 0 0 0 0 0 0 0 0 0 19 | YFLN 0 0 0 0 0 0 0 0 0 0 0 0 20 | FWGN 1 0 0 0 0 0 0 0 0 0 0 0 21 | VDXN 0 0 0 0 0 0 0 0 0 0 0 0 22 | VDYN 0 0 0 0 0 0 0 0 0 0 0 0 23 | VCXN 0 0 0 0 0 0 0 0 0 0 0 0 24 | VCYN 0 0 0 0 0 0 0 0 0 0 0 0 25 | VANN 0 0 0 0 0 0 0 0 0 0 0 0 26 | WAVM 1 4.861E-1 1 27 | WAVM 2 5.876E-1 1 28 | WAVM 3 6.563E-1 1 29 | WAVM 4 5.5E-1 1 30 | WAVM 5 5.5E-1 1 31 | WAVM 6 5.5E-1 1 32 | WAVM 7 5.5E-1 1 33 | WAVM 8 5.5E-1 1 34 | WAVM 9 5.5E-1 1 35 | WAVM 10 5.5E-1 1 36 | WAVM 11 5.5E-1 1 37 | WAVM 12 5.5E-1 1 38 | WAVM 13 5.5E-1 1 39 | WAVM 14 5.5E-1 1 40 | WAVM 15 5.5E-1 1 41 | WAVM 16 5.5E-1 1 42 | WAVM 17 5.5E-1 1 43 | WAVM 18 5.5E-1 1 44 | WAVM 19 5.5E-1 1 45 | WAVM 20 5.5E-1 1 46 | WAVM 21 5.5E-1 1 47 | WAVM 22 5.5E-1 1 48 | WAVM 23 5.5E-1 1 49 | WAVM 24 5.5E-1 1 50 | PWAV 2 51 | POLS 1 0 1 0 0 1 0 52 | GLRS 1 0 53 | GSTD 0 100.000 100.000 100.000 100.000 100.000 100.000 0 1 1 0 0 1 1 1 1 1 1 54 | NSCD 100 500 0 1.0E-3 5 1.0E-6 0 0 0 0 0 0 1000000 0 2 55 | COFN QF "COATINGTHOR.DAT" "SCATTER_PROFILE.DAT" "ABG_DATA.DAT" "PROFILE.GRD" 56 | COFN COATINGTHOR.DAT SCATTER_PROFILE.DAT ABG_DATA.DAT PROFILE.GRD 57 | SURF 0 58 | TYPE STANDARD 59 | CURV 0.0 0 0 0 0 "" 60 | HIDE 0 0 0 0 0 0 0 0 0 0 61 | MIRR 2 0 62 | SLAB 3 63 | DISZ INFINITY 64 | DIAM 0 0 0 0 1 "" 65 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 66 | SURF 1 67 | COMM AC254-100-A 68 | STOP 69 | TYPE STANDARD 70 | CURV 1.593625498007969800E-002 0 0 0 0 "" 71 | COAT THORA 72 | HIDE 0 0 0 0 0 0 0 0 0 0 73 | MIRR 2 0 74 | SLAB 4 75 | DISZ 4.0 76 | GLAS N-BK7 0 0 0 0 0 0 0 0 0 0 77 | DIAM 1.27E+1 1 0 0 1 "" 78 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 79 | FLAP 0 1.27E+1 0 80 | SURF 2 81 | TYPE STANDARD 82 | CURV -2.187705097352880100E-002 0 0 0 0 "" 83 | HIDE 0 0 0 0 0 0 0 0 0 0 84 | MIRR 2 0 85 | SLAB 5 86 | DISZ 2.5 87 | GLAS SF5 0 0 0 0 0 0 0 0 0 0 88 | DIAM 1.27E+1 1 0 0 1 "" 89 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 90 | FLAP 0 1.27E+1 0 91 | SURF 3 92 | TYPE STANDARD 93 | CURV -7.798487093503899700E-003 0 0 0 0 "" 94 | COAT THORASLAH64 95 | HIDE 0 0 0 0 0 0 0 0 0 0 96 | MIRR 2 0 97 | SLAB 1 98 | DISZ 9.706800996493E+1 99 | DIAM 1.27E+1 1 0 0 1 "" 100 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 101 | FLAP 0 1.27E+1 0 102 | SURF 4 103 | TYPE STANDARD 104 | CURV 0.0 0 0 0 0 "" 105 | HIDE 0 0 0 0 0 0 0 0 0 0 106 | MIRR 2 0 107 | SLAB 2 108 | DISZ 0 109 | DIAM 6.803919974926E-3 0 0 0 1 "" 110 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 111 | BLNK 112 | TOL TOFF 0 0 0 0 0 0 0 113 | MNUM 1 1 114 | MOFF 0 1 "" 0 0 0 1 1 0 0.0 "" 115 | -------------------------------------------------------------------------------- /raytracing/specifications/AC254-125-A-Zemax(ZMX).zmx: -------------------------------------------------------------------------------- 1 | VERS 120928 106 30221 2 | MODE SEQ 3 | NAME 25.4 mm, f=125.0 mm, Near IR Achromat, ARC: 350-700 nm 4 | NOTE 0 FOR INFORMATION ONLY, NOT FOR MANUFACTURING. ALL VALUES AND COATING(S) ARE TYPICAL AND THEORETICAL, ACTUAL PERFORMANCE MAY VARY. FOR FURTHER INFORMATION CONTACT CUSTOMER SERVICE AT TECHSUPPORT@THORL 5 | NOTE 0 ABS.COM 6 | PFIL 0 0 0 7 | UNIT MM X W X CM MR CPMM 8 | ENPD 2.286E+1 9 | ENVD 2.0E+1 1 0 10 | GFAC 0 0 11 | GCAT SCHOTT INFRARED MISC 12 | RAIM 0 0 1 1 0 0 0 0 0 13 | PUSH 0 0 0 0 0 0 14 | SDMA 0 1 0 15 | FTYP 0 0 1 3 0 0 0 16 | ROPD 2 17 | PICB 1 18 | XFLN 0 0 0 0 0 0 0 0 0 0 0 0 19 | YFLN 0 0 0 0 0 0 0 0 0 0 0 0 20 | FWGN 1 0 0 0 0 0 0 0 0 0 0 0 21 | VDXN 0 0 0 0 0 0 0 0 0 0 0 0 22 | VDYN 0 0 0 0 0 0 0 0 0 0 0 0 23 | VCXN 0 0 0 0 0 0 0 0 0 0 0 0 24 | VCYN 0 0 0 0 0 0 0 0 0 0 0 0 25 | VANN 0 0 0 0 0 0 0 0 0 0 0 0 26 | WAVM 1 4.861E-1 1 27 | WAVM 2 5.876E-1 1 28 | WAVM 3 6.563E-1 1 29 | WAVM 4 5.5E-1 1 30 | WAVM 5 5.5E-1 1 31 | WAVM 6 5.5E-1 1 32 | WAVM 7 5.5E-1 1 33 | WAVM 8 5.5E-1 1 34 | WAVM 9 5.5E-1 1 35 | WAVM 10 5.5E-1 1 36 | WAVM 11 5.5E-1 1 37 | WAVM 12 5.5E-1 1 38 | WAVM 13 5.5E-1 1 39 | WAVM 14 5.5E-1 1 40 | WAVM 15 5.5E-1 1 41 | WAVM 16 5.5E-1 1 42 | WAVM 17 5.5E-1 1 43 | WAVM 18 5.5E-1 1 44 | WAVM 19 5.5E-1 1 45 | WAVM 20 5.5E-1 1 46 | WAVM 21 5.5E-1 1 47 | WAVM 22 5.5E-1 1 48 | WAVM 23 5.5E-1 1 49 | WAVM 24 5.5E-1 1 50 | PWAV 2 51 | POLS 1 0 1 0 0 1 0 52 | GLRS 1 0 53 | GSTD 0 100.000 100.000 100.000 100.000 100.000 100.000 0 1 1 0 0 1 1 1 1 1 1 54 | NSCD 100 500 0 1.0E-3 5 1.0E-6 0 0 0 0 0 0 1000000 0 2 55 | COFN QF "COATINGTHOR.DAT" "SCATTER_PROFILE.DAT" "ABG_DATA.DAT" "PROFILE.GRD" 56 | COFN COATINGTHOR.DAT SCATTER_PROFILE.DAT ABG_DATA.DAT PROFILE.GRD 57 | SURF 0 58 | TYPE STANDARD 59 | CURV 0.0 0 0 0 0 "" 60 | HIDE 0 0 0 0 0 0 0 0 0 0 61 | MIRR 2 0 62 | SLAB 3 63 | DISZ INFINITY 64 | DIAM 0 0 0 0 1 "" 65 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 66 | SURF 1 67 | COMM AC254-125-A 68 | STOP 69 | TYPE STANDARD 70 | CURV 1.288164982260432700E-002 0 0 0 0 "" 71 | COAT THORA 72 | HIDE 0 0 0 0 0 0 0 0 0 0 73 | MIRR 2 0 74 | SLAB 4 75 | DISZ 4.003571813499 76 | GLAS N-BK7 0 0 0 0 0 0 0 0 0 0 77 | DIAM 1.27E+1 1 0 0 1 "" 78 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 79 | FLAP 0 1.27E+1 0 80 | SURF 2 81 | TYPE STANDARD 82 | CURV -1.788258664719243400E-002 0 0 0 0 "" 83 | COAT THORA 84 | HIDE 0 0 0 0 0 0 0 0 0 0 85 | MIRR 2 0 86 | SLAB 5 87 | DISZ 2.833474807849 88 | GLAS N-SF5 0 0 0 0 0 0 0 0 0 0 89 | DIAM 1.27E+1 1 0 0 1 "" 90 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 91 | FLAP 0 1.27E+1 0 92 | SURF 3 93 | TYPE STANDARD 94 | CURV -6.218054603015779400E-003 0 0 0 0 "" 95 | HIDE 0 0 0 0 0 0 0 0 0 0 96 | MIRR 2 0 97 | SLAB 1 98 | DISZ 1.21975878954E+2 99 | MAZH 0 0 100 | DIAM 1.27E+1 1 0 0 1 "" 101 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 102 | FLAP 0 1.27E+1 0 103 | SURF 4 104 | TYPE STANDARD 105 | CURV 0.0 0 0 0 0 "" 106 | HIDE 0 0 0 0 0 0 0 0 0 0 107 | MIRR 2 0 108 | SLAB 2 109 | DISZ 0 110 | DIAM 5.780458450641E-3 0 0 0 1 "" 111 | POPS 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 112 | BLNK 113 | TOL TOFF 0 0 0 0 0 0 0 114 | MNUM 1 1 115 | MOFF 0 1 "" 0 0 0 1 1 0 0.0 "" 116 | -------------------------------------------------------------------------------- /raytracing/specifications/ASL5040-UV-Zemax(ZMX).zmx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/specifications/ASL5040-UV-Zemax(ZMX).zmx -------------------------------------------------------------------------------- /raytracing/specifications/envtest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import io 4 | from contextlib import redirect_stdout, redirect_stderr 5 | import unittest 6 | import tempfile 7 | import matplotlib.pyplot as plt 8 | from unittest.mock import Mock, patch 9 | import numpy as np 10 | import warnings 11 | 12 | import io 13 | import contextlib 14 | 15 | #Use with patch('matplotlib.pyplot.show', new=Mock()): 16 | # or @patch('matplotlib.pyplot.show', new=Mock()) 17 | 18 | perfTestKey = "RAYTRACING_PERF_TESTS" 19 | performanceTests = False 20 | if perfTestKey in os.environ: 21 | performanceTests = os.environ[perfTestKey] 22 | else: 23 | performanceTests = False 24 | 25 | class RaytracingTestCase(unittest.TestCase): 26 | tempDir = os.path.join(tempfile.gettempdir(), "tempDir") 27 | setupCalled = False 28 | stdout = None 29 | stderr = None 30 | def __init__(self, tests=()): 31 | super(RaytracingTestCase, self).__init__(tests) 32 | warnings.simplefilter("ignore") 33 | 34 | def setUp(self): 35 | super().setUp() 36 | warnings.simplefilter("ignore") 37 | 38 | # Seed with same seed every time 39 | np.random.seed(0) 40 | 41 | self.setupCalled = True 42 | 43 | def tearDown(self) -> None: 44 | self.clearMatplotlibPlots() 45 | 46 | def testProperlySetup(self): 47 | self.assertTrue(self.setupCalled, msg="You must call super().setUp() in your own setUp()") 48 | 49 | @classmethod 50 | def clearMatplotlibPlots(cls): 51 | plt.clf() 52 | plt.cla() 53 | plt.close() 54 | 55 | def assertDoesNotRaise(self, func, exceptionType=None, *funcArgs, **funcKwargs): 56 | returnValue = None 57 | if exceptionType is None: 58 | exceptionType = Exception 59 | try: 60 | returnValue = func(*funcArgs, **funcKwargs) 61 | except exceptionType as e: 62 | self.fail(f"An exception was raised:\n{e}") 63 | # Don't handle exceptions not in exceptionType 64 | return returnValue 65 | 66 | def assertPrints(self, func, out, stripOutput: bool = True, *funcArgs, **funcKwargs): 67 | @redirectStdOutToFile 68 | def getOutput(): 69 | func(*funcArgs, **funcKwargs) 70 | 71 | value = getOutput() 72 | if stripOutput: 73 | value = value.strip() 74 | self.assertEqual(value, out) 75 | 76 | 77 | @classmethod 78 | def createTempDirectory(cls): 79 | if os.path.exists(cls.tempDir): 80 | cls.deleteTempDirectory() 81 | os.mkdir(cls.tempDir) 82 | 83 | @classmethod 84 | def deleteTempDirectory(cls): 85 | if os.path.exists(cls.tempDir): 86 | for file in os.listdir(cls.tempDir): 87 | os.remove(os.path.join(cls.tempDir, file)) 88 | os.rmdir(cls.tempDir) 89 | 90 | @classmethod 91 | def setUpClass(cls) -> None: 92 | cls.createTempDirectory() 93 | 94 | @classmethod 95 | def tearDownClass(cls) -> None: 96 | cls.deleteTempDirectory() 97 | 98 | def tempFilePath(self, filename="temp.dat") -> str: 99 | return os.path.join(RaytracingTestCase.tempDir, filename) 100 | 101 | 102 | def redirectStdOutToFile(_func=None, file=None, returnOnlyValue: bool = True): 103 | if file is None: 104 | file = io.StringIO() 105 | 106 | def redirectStdOut(func): 107 | def wrapperRedirectStdOut(*args, **kwargs): 108 | with redirect_stdout(file): 109 | func(*args, **kwargs) 110 | 111 | return file.getvalue() if returnOnlyValue else file 112 | 113 | return wrapperRedirectStdOut 114 | 115 | if _func is None: 116 | return redirectStdOut 117 | return redirectStdOut(_func) 118 | 119 | 120 | def main(): 121 | unittest.main() 122 | 123 | 124 | def skip(reason: str): 125 | return unittest.skip(reason) 126 | 127 | 128 | def skipIf(condition: object, reason: str): 129 | return unittest.skipIf(condition, reason) 130 | 131 | 132 | def skipUnless(condition: object, reason: str): 133 | return unittest.skipUnless(condition, reason) 134 | 135 | 136 | def expectedFailure(func): 137 | return unittest.expectedFailure(func) 138 | 139 | def patchMatplotLib(): 140 | return patch('matplotlib.pyplot.show', new=Mock()) 141 | 142 | # append module root directory to sys.path 143 | sys.path.insert(0, 144 | os.path.dirname( 145 | os.path.dirname( 146 | os.path.dirname( 147 | os.path.abspath(__file__) 148 | ) 149 | ) 150 | ) 151 | ) 152 | -------------------------------------------------------------------------------- /raytracing/specifications/lensBuilder.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | import csv 3 | from raytracing import * 4 | 5 | """ 6 | This is just for experimenting with building lenses from prescriptions. 7 | We use an Olympus patent as a test, since everything is there 8 | 9 | """ 10 | class PrescriptionFile: 11 | def __init__(self, filepath, columnId=None): 12 | self.filepath = filepath 13 | self.headers = [] 14 | self.rows = self.readRows(self.filepath) 15 | 16 | def readRows(self, filepath): 17 | rows = [] 18 | with open(filepath) as csvfile: 19 | dialect = csv.Sniffer().sniff(csvfile.read(1024)) 20 | csvfile.seek(0) 21 | if csv.Sniffer().has_header(csvfile.read(1024)): 22 | csvfile.seek(0) 23 | self.headers = csvfile.readline().split(dialect.delimiter) 24 | else: 25 | csvfile.seek(0) 26 | fileReader = csv.reader(csvfile, dialect, 27 | quoting=csv.QUOTE_NONNUMERIC) 28 | 29 | for row in fileReader: 30 | rows.append(row) 31 | return rows 32 | 33 | 34 | file = PrescriptionFile("olympus-table3.csv") 35 | 36 | n1 = 1.33 # water immersion 37 | olympus20xPrescription = MatrixGroup(label="OlympusTest") 38 | for row in file.rows: 39 | i, R, d, n2, eta = row 40 | 41 | try: 42 | n2 = float(n2) 43 | except: 44 | n2 = 1.0 45 | 46 | element1 = DielectricInterface(n1=n1, n2=n2, R=R, diameter=20, label="#int{0}".format(i)) 47 | element2 = Space(n=n2, d=float(d), label="space-{0}".format(i)) 48 | 49 | olympus20xPrescription.append(element1) 50 | olympus20xPrescription.append(element2) 51 | n1 = n2 52 | 53 | print("--------------") 54 | print("From Raytracing calculations") 55 | print("f = {0:0.3f} mm".format(olympus20xPrescription.effectiveFocalLengths().f1)) 56 | print("WD = {0:.3f} mm".format(olympus20xPrescription.frontFocalLength())) 57 | print("--------------") 58 | print("From Olympus patent, Table 3, at https://patents.google.com/patent/US6501603B2/en") 59 | print("f = 9.006 mm") 60 | print("WD = 2.04 mm") 61 | 62 | olympus20xPrescription = CompoundLens(olympus20xPrescription.elements, 63 | designFocalLength=olympus20xPrescription.effectiveFocalLengths().f1, 64 | diameter=20) 65 | olympus20xPrescription.display() 66 | -------------------------------------------------------------------------------- /raytracing/specifications/olympus-table3.csv: -------------------------------------------------------------------------------- 1 | #int R D n abbe 2 | 1 +inf 1.6100 1.45853 67.94 3 | 2 -2.4060 5.7597 1.75500 52.32 4 | 3 -6.5059 0.2000 5 | 4 -25.5003 3.1500 1.49700 81.08 6 | 5 -10.9323 0.1200 7 | 6 56.1096 6.4800 1.49700 81.08 8 | 7 -11.9889 1.9500 1.52944 51.72 9 | 8 -25.3665 0.1200 10 | 9 223.7456 1.9500 1.75500 52.32 11 | 10 17.0397 6.5000 1.43875 94.99 12 | 11 -25.4913 0.1200 13 | 12 12.1419 4.8264 1.43875 94.99 14 | 13 30.1344 0.2000 15 | 14 10.8233 4.7695 1.43875 94.99 16 | 15 26.5356 1.5024 1.59551 39.21 17 | 16 6.2707 6.1162 18 | 17 -6.9886 1.5000 1.61340 43.84 19 | 18 18.1665 6.3698 1.43875 94.99 20 | 19 -11.5665 0.2500 21 | 20 -26.3050 3.3649 1.49700 81.08 22 | 21 -11.8757 0.1200 23 | 22 10.9414 7.0500 1.43875 94.99 24 | 23 -25.6275 1.6500 1.52944 51.72 25 | 24 8.8733 5.6500 26 | 25 -8.4093 1.7500 1.51633 64.14 27 | 26 95.8203 5.1500 1.61293 36.99 28 | 27 -12.3982 0 1.0 0 -------------------------------------------------------------------------------- /raytracing/specifications/references.md: -------------------------------------------------------------------------------- 1 | # Olympus patents for objectives 2 | # https://patents.google.com/patent/US6501603B2/en 3 | # Table 3 4 | 5 | -------------------------------------------------------------------------------- /raytracing/specifications/thorlabs.Achromatic_Doublet_AR_Coatings.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/specifications/thorlabs.Achromatic_Doublet_AR_Coatings.xlsx -------------------------------------------------------------------------------- /raytracing/specifications/zmax_49270.zmx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/specifications/zmax_49270.zmx -------------------------------------------------------------------------------- /raytracing/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | 5 | omit = 6 | # omit anything in a .local directory anywhere 7 | */.local/* 8 | # omit everything in /usr 9 | /usr/* 10 | # omit this single file 11 | utils/tirefire.py 12 | tests/* 13 | 14 | [report] 15 | # Regexes for lines to exclude from consideration 16 | exclude_lines = 17 | # Have to re-enable the standard pragma 18 | pragma: no cover 19 | 20 | # Don't complain about missing debug-only code: 21 | def __repr__ 22 | if self\.debug 23 | 24 | # Don't complain if tests don't hit defensive assertion code: 25 | raise AssertionError 26 | raise NotImplementedError 27 | 28 | # Don't complain if non-runnable code isn't run: 29 | if 0: 30 | if __name__ == .__main__.: 31 | 32 | ignore_errors = True 33 | 34 | [html] 35 | directory = coverage_html_report 36 | -------------------------------------------------------------------------------- /raytracing/tests/envtest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import io 4 | from contextlib import redirect_stdout, redirect_stderr 5 | import unittest 6 | import tempfile 7 | import matplotlib.pyplot as plt 8 | from unittest.mock import Mock, patch 9 | import numpy as np 10 | import warnings 11 | 12 | import io 13 | import contextlib 14 | 15 | #Use with patch('matplotlib.pyplot.show', new=Mock()): 16 | # or @patch('matplotlib.pyplot.show', new=Mock()) 17 | 18 | perfTestKey = "RAYTRACING_PERF_TESTS" 19 | performanceTests = False 20 | if perfTestKey in os.environ: 21 | performanceTests = os.environ[perfTestKey] 22 | else: 23 | performanceTests = False 24 | 25 | class RaytracingTestCase(unittest.TestCase): 26 | tempDir = os.path.join(tempfile.gettempdir(), "tempDir") 27 | setupCalled = False 28 | stdout = None 29 | stderr = None 30 | def __init__(self, tests=()): 31 | super(RaytracingTestCase, self).__init__(tests) 32 | warnings.simplefilter("ignore") 33 | 34 | def setUp(self): 35 | super().setUp() 36 | warnings.simplefilter("ignore") 37 | 38 | # Seed with same seed every time 39 | np.random.seed(0) 40 | 41 | self.setupCalled = True 42 | 43 | def tearDown(self) -> None: 44 | self.clearMatplotlibPlots() 45 | 46 | def testProperlySetup(self): 47 | self.assertTrue(self.setupCalled, msg="You must call super().setUp() in your own setUp()") 48 | 49 | @classmethod 50 | def clearMatplotlibPlots(cls): 51 | plt.clf() 52 | plt.cla() 53 | plt.close() 54 | 55 | def assertDoesNotRaise(self, func, exceptionType=None, *funcArgs, **funcKwargs): 56 | returnValue = None 57 | if exceptionType is None: 58 | exceptionType = Exception 59 | try: 60 | returnValue = func(*funcArgs, **funcKwargs) 61 | except exceptionType as e: 62 | self.fail(f"An exception was raised:\n{e}") 63 | # Don't handle exceptions not in exceptionType 64 | return returnValue 65 | 66 | def assertPrints(self, func, out, stripOutput: bool = True, *funcArgs, **funcKwargs): 67 | @redirectStdOutToFile 68 | def getOutput(): 69 | func(*funcArgs, **funcKwargs) 70 | 71 | value = getOutput() 72 | if stripOutput: 73 | value = value.strip() 74 | self.assertEqual(value, out) 75 | 76 | 77 | @classmethod 78 | def createTempDirectory(cls): 79 | if os.path.exists(cls.tempDir): 80 | cls.deleteTempDirectory() 81 | os.mkdir(cls.tempDir) 82 | 83 | @classmethod 84 | def deleteTempDirectory(cls): 85 | if os.path.exists(cls.tempDir): 86 | for file in os.listdir(cls.tempDir): 87 | os.remove(os.path.join(cls.tempDir, file)) 88 | os.rmdir(cls.tempDir) 89 | 90 | @classmethod 91 | def setUpClass(cls) -> None: 92 | cls.createTempDirectory() 93 | 94 | @classmethod 95 | def tearDownClass(cls) -> None: 96 | cls.deleteTempDirectory() 97 | 98 | def tempFilePath(self, filename="temp.dat") -> str: 99 | return os.path.join(RaytracingTestCase.tempDir, filename) 100 | 101 | 102 | def redirectStdOutToFile(_func=None, file=None, returnOnlyValue: bool = True): 103 | if file is None: 104 | file = io.StringIO() 105 | 106 | def redirectStdOut(func): 107 | def wrapperRedirectStdOut(*args, **kwargs): 108 | with redirect_stdout(file): 109 | func(*args, **kwargs) 110 | 111 | return file.getvalue() if returnOnlyValue else file 112 | 113 | return wrapperRedirectStdOut 114 | 115 | if _func is None: 116 | return redirectStdOut 117 | return redirectStdOut(_func) 118 | 119 | 120 | def main(): 121 | unittest.main() 122 | 123 | 124 | def skip(reason: str): 125 | return unittest.skip(reason) 126 | 127 | 128 | def skipIf(condition: object, reason: str): 129 | return unittest.skipIf(condition, reason) 130 | 131 | 132 | def skipUnless(condition: object, reason: str): 133 | return unittest.skipUnless(condition, reason) 134 | 135 | 136 | def expectedFailure(func): 137 | return unittest.expectedFailure(func) 138 | 139 | def patchMatplotLib(): 140 | return patch('matplotlib.pyplot.show', new=Mock()) 141 | 142 | # append module root directory to sys.path 143 | sys.path.insert(0, 144 | os.path.dirname( 145 | os.path.dirname( 146 | os.path.dirname( 147 | os.path.abspath(__file__) 148 | ) 149 | ) 150 | ) 151 | ) 152 | -------------------------------------------------------------------------------- /raytracing/tests/parallel_time_smallcounts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/tests/parallel_time_smallcounts.png -------------------------------------------------------------------------------- /raytracing/tests/shifts-ac254-100-A.txt: -------------------------------------------------------------------------------- 1 | #Wavelength (nm) Focal Length Shift (mm) 2 | 400.00 0.29598107 3 | 402.50 0.27565484 4 | 405.00 0.25646567 5 | 407.50 0.23835086 6 | 410.00 0.22125176 7 | 412.50 0.20511344 8 | 415.00 0.18988444 9 | 417.50 0.17551653 10 | 420.00 0.16196443 11 | 422.50 0.14918564 12 | 425.00 0.13714024 13 | 427.50 0.12579067 14 | 430.00 0.11510164 15 | 432.50 0.1050399 16 | 435.00 0.09557416 17 | 437.50 0.08667492 18 | 440.00 0.07831436 19 | 442.50 0.07046627 20 | 445.00 0.06310586 21 | 447.50 0.05620977 22 | 450.00 0.0497559 23 | 452.50 0.04372337 24 | 455.00 0.03809243 25 | 457.50 0.03284439 26 | 460.00 0.02796156 27 | 462.50 0.02342721 28 | 465.00 0.01922546 29 | 467.50 0.01534127 30 | 470.00 0.01176037 31 | 472.50 0.00846926 32 | 475.00 0.00545508 33 | 477.50 0.00270565 34 | 480.00 0.0002094 35 | 482.50 -0.00204466 36 | 485.00 -0.00406698 37 | 487.50 -0.00586748 38 | 490.00 -0.00745563 39 | 492.50 -0.00884039 40 | 495.00 -0.01003033 41 | 497.50 -0.01103358 42 | 500.00 -0.01185788 43 | 502.50 -0.01251063 44 | 505.00 -0.01299886 45 | 507.50 -0.01332926 46 | 510.00 -0.01350824 47 | 512.50 -0.01354188 48 | 515.00 -0.013436 49 | 517.50 -0.01319614 50 | 520.00 -0.0128276 51 | 522.50 -0.01233544 52 | 525.00 -0.01172447 53 | 527.50 -0.0109993 54 | 530.00 -0.01016434 55 | 532.50 -0.00922379 56 | 535.00 -0.00818168 57 | 537.50 -0.00704185 58 | 540.00 -0.00580796 59 | 542.50 -0.00448355 60 | 545.00 -0.00307196 61 | 547.50 -0.00157642 62 | 550.00 0 63 | 552.50 0.00165435 64 | 555.00 0.0033838 65 | 557.50 0.00518567 66 | 560.00 0.00705736 67 | 562.50 0.00899639 68 | 565.00 0.01100039 69 | 567.50 0.01306709 70 | 570.00 0.0151943 71 | 572.50 0.01737994 72 | 575.00 0.01962201 73 | 577.50 0.02191858 74 | 580.00 0.02426782 75 | 582.50 0.02666795 76 | 585.00 0.02911729 77 | 587.50 0.0316142 78 | 590.00 0.03415714 79 | 592.50 0.0367446 80 | 595.00 0.03937515 81 | 597.50 0.04204741 82 | 600.00 0.04476005 83 | 602.50 0.04751181 84 | 605.00 0.05030147 85 | 607.50 0.05312786 86 | 610.00 0.05598985 87 | 612.50 0.05888636 88 | 615.00 0.06181636 89 | 617.50 0.06477884 90 | 620.00 0.06777286 91 | 622.50 0.07079749 92 | 625.00 0.07385184 93 | 627.50 0.07693508 94 | 630.00 0.08004637 95 | 632.50 0.08318495 96 | 635.00 0.08635004 97 | 637.50 0.08954094 98 | 640.00 0.09275694 99 | 642.50 0.09599737 100 | 645.00 0.0992616 101 | 647.50 0.10254899 102 | 650.00 0.10585896 103 | 652.50 0.10919093 104 | 655.00 0.11254436 105 | 657.50 0.11591871 106 | 660.00 0.11931348 107 | 662.50 0.12272818 108 | 665.00 0.12616233 109 | 667.50 0.12961549 110 | 670.00 0.13308723 111 | 672.50 0.13657711 112 | 675.00 0.14008475 113 | 677.50 0.14360975 114 | 680.00 0.14715175 115 | 682.50 0.15071038 116 | 685.00 0.1542853 117 | 687.50 0.15787619 118 | 690.00 0.16148272 119 | 692.50 0.1651046 120 | 695.00 0.16874151 121 | 697.50 0.1723932 122 | 700.00 0.17605937 -------------------------------------------------------------------------------- /raytracing/tests/shifts-ac254-200-A.txt: -------------------------------------------------------------------------------- 1 | #Wavelength (nm) Focal Length Shift (mm) 2 | 400.00 0.76143878 3 | 402.50 0.7147652 4 | 405.00 0.67061924 5 | 407.50 0.62886167 6 | 410.00 0.5893621 7 | 412.50 0.5519983 8 | 415.00 0.51665566 9 | 417.50 0.48322661 10 | 420.00 0.45161015 11 | 422.50 0.42171134 12 | 425.00 0.39344093 13 | 427.50 0.36671496 14 | 430.00 0.34145436 15 | 432.50 0.31758467 16 | 435.00 0.29503572 17 | 437.50 0.27374134 18 | 440.00 0.25363909 19 | 442.50 0.23467004 20 | 445.00 0.21677854 21 | 447.50 0.19991199 22 | 450.00 0.18402069 23 | 452.50 0.16905763 24 | 455.00 0.15497832 25 | 457.50 0.14174065 26 | 460.00 0.12930475 27 | 462.50 0.11763284 28 | 465.00 0.10668911 29 | 467.50 0.09643963 30 | 470.00 0.08685218 31 | 472.50 0.07789623 32 | 475.00 0.06954275 33 | 477.50 0.06176423 34 | 480.00 0.05453449 35 | 482.50 0.04782868 36 | 485.00 0.04162317 37 | 487.50 0.03589549 38 | 490.00 0.03062429 39 | 492.50 0.02578921 40 | 495.00 0.02137092 41 | 497.50 0.01735099 42 | 500.00 0.01371188 43 | 502.50 0.01043688 44 | 505.00 0.00751005 45 | 507.50 0.00491624 46 | 510.00 0.00264095 47 | 512.50 0.00067041 48 | 515.00 -0.00100856 49 | 517.50 -0.0024085 50 | 520.00 -0.0035414 51 | 522.50 -0.00441871 52 | 525.00 -0.00505133 53 | 527.50 -0.00544969 54 | 530.00 -0.00562377 55 | 532.50 -0.00558307 56 | 535.00 -0.00533668 57 | 537.50 -0.00489331 58 | 540.00 -0.00426126 59 | 542.50 -0.00344846 60 | 545.00 -0.00246253 61 | 547.50 -0.00131073 62 | 550.00 0 63 | 552.50 0.00146299 64 | 555.00 0.00307189 65 | 557.50 0.00482059 66 | 560.00 0.00670327 67 | 562.50 0.00871434 68 | 565.00 0.01084845 69 | 567.50 0.01310047 70 | 570.00 0.01546551 71 | 572.50 0.01793885 72 | 575.00 0.02051601 73 | 577.50 0.02319264 74 | 580.00 0.02596463 75 | 582.50 0.02882801 76 | 585.00 0.03177897 77 | 587.50 0.03481387 78 | 590.00 0.03792921 79 | 592.50 0.04112165 80 | 595.00 0.04438797 81 | 597.50 0.04772508 82 | 600.00 0.05113003 83 | 602.50 0.05459999 84 | 605.00 0.05813223 85 | 607.50 0.06172413 86 | 610.00 0.0653732 87 | 612.50 0.06907703 88 | 615.00 0.07283331 89 | 617.50 0.07663982 90 | 620.00 0.08049445 91 | 622.50 0.08439514 92 | 625.00 0.08833995 93 | 627.50 0.09232698 94 | 630.00 0.09635445 95 | 632.50 0.1004206 96 | 635.00 0.10452378 97 | 637.50 0.10866239 98 | 640.00 0.1128349 99 | 642.50 0.11703984 100 | 645.00 0.12127578 101 | 647.50 0.12554137 102 | 650.00 0.12983532 103 | 652.50 0.13415636 104 | 655.00 0.13850329 105 | 657.50 0.14287497 106 | 660.00 0.14727028 107 | 662.50 0.15168815 108 | 665.00 0.15612758 109 | 667.50 0.16058757 110 | 670.00 0.16506718 111 | 672.50 0.16956551 112 | 675.00 0.1740817 113 | 677.50 0.17861491 114 | 680.00 0.18316434 115 | 682.50 0.18772922 116 | 685.00 0.19230882 117 | 687.50 0.19690242 118 | 690.00 0.20150936 119 | 692.50 0.20612898 120 | 695.00 0.21076066 121 | 697.50 0.21540379 122 | 700.00 0.22005781 123 | -------------------------------------------------------------------------------- /raytracing/tests/testDisplay.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | from numpy import linspace, pi 4 | from raytracing.materials import * 5 | 6 | inf = float("+inf") 7 | 8 | def exampleCodeThatUsedToCrashe(): 9 | # I created the same lens manually to see what it looked like 10 | # From the prescription, valid only at 405 nm. 11 | asl5040 = MatrixGroup() 12 | 13 | asl5040.append(DielectricInterface(R=17.37,n1=1.0, n2=materials.FusedSilica.n(0.405), diameter=50)) 14 | asl5040.append(Space(d=29.85, n=materials.FusedSilica.n(0.405))) 15 | asl5040.append(DielectricInterface(R=float("+inf"),n1=materials.FusedSilica.n(0.405),n2=1.0,diameter=50)) 16 | 17 | # The calculated focal length is not 40 mm, it is rather 36.99, probably 18 | # because I do not consider the real, complete surface I only approximate 19 | # with a spherical surface 20 | print("We expect f=40mm from the specs if we use the full surface") 21 | print("f_e = {0}".format(asl5040.effectiveFocalLengths())) 22 | print("FFL = {0}".format(asl5040.frontFocalLength())) 23 | print("BFL = {0}".format(asl5040.backFocalLength())) 24 | 25 | 26 | collimationPath = ImagingPath(label="Lens constructed manually") 27 | # Start at front focal spot where we would put the source to collimate 28 | collimationPath.append(Space(d=asl5040.frontFocalLength())) 29 | collimationPath.append(asl5040) 30 | collimationPath.append(Space(d=50)) 31 | # Again, I fixed the bug and will release a new version today or tomorrow. 32 | # I included the graph in the email I sent you. It looks the same and behaves as expected 33 | collimationPath.display(ObjectRays(diameter=1, halfAngle=0.5,H=5,T=5)) 34 | 35 | class TestDisplay(envtest.RaytracingTestCase): 36 | def testIfFixedDoesNotCrash(self): 37 | exampleCodeThatUsedToCrashe() 38 | 39 | 40 | if __name__ == '__main__': 41 | envtest.main() 42 | 43 | -------------------------------------------------------------------------------- /raytracing/tests/testsAxicon.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | 3 | from raytracing import * 4 | from numpy import random 5 | from numpy import * 6 | 7 | inf = float("+inf") 8 | degrees = math.pi/180 9 | 10 | class TestAxicon(envtest.RaytracingTestCase): 11 | 12 | def testAxicon(self): 13 | n = 1.5 14 | alpha = 2.6*degrees 15 | diameter = 100 16 | label = "Axicon" 17 | axicon = Axicon(alpha, n, diameter, label) 18 | self.assertEqual(axicon.n, n) 19 | self.assertEqual(axicon.alpha, alpha) 20 | self.assertEqual(axicon.apertureDiameter, diameter) 21 | self.assertEqual(axicon.label, label) 22 | self.assertEqual(axicon.frontIndex, 1.0) 23 | self.assertEqual(axicon.backIndex, 1.0) 24 | 25 | def testDeviationAngleIs0(self): 26 | n = 1 27 | alpha = random.randint(2000, 5000, 1).item() / 1000*degrees 28 | axicon = Axicon(alpha, n) 29 | self.assertEqual(axicon.deviationAngle(), 0) 30 | 31 | def testDeviationAngleIs0Too(self): 32 | n = random.randint(1000, 3000, 1).item() / 1000 33 | alpha = 0*degrees 34 | axicon = Axicon(alpha, n) 35 | self.assertEqual(axicon.deviationAngle(), 0) 36 | 37 | def testDeviationAngle(self): 38 | n = 1.33 39 | alpha = 4*degrees 40 | axicon = Axicon(alpha, n) 41 | self.assertAlmostEqual(axicon.deviationAngle(), 1.32*degrees, places=15) 42 | 43 | def testFocalLineLengthYIsNoneAndInfiniteDiameter(self): 44 | n = random.randint(1000, 3000, 1).item() / 1000 45 | alpha = random.randint(2000, 5000, 1).item() / 1000*degrees 46 | axicon = Axicon(alpha, n) 47 | self.assertEqual(axicon.focalLineLength(), inf) 48 | 49 | def testFocalLineLengthYIsNone(self): 50 | n = 1.5 51 | alpha = 2.6*degrees 52 | axicon = Axicon(alpha, n, 100) 53 | y = 50 54 | L = y/tan(axicon.deviationAngle()) 55 | self.assertAlmostEqual(axicon.focalLineLength(), L,0) 56 | 57 | def testFocalLineLengthSignOfY(self): 58 | n = 1.43 59 | alpha = 1.95*degrees 60 | axicon = Axicon(alpha=alpha, n=n, diameter=100) 61 | self.assertAlmostEqual(axicon.focalLineLength(-2), axicon.focalLineLength(2)) 62 | 63 | def testFocalLineLengthPositiveY(self): 64 | n = 1.43 65 | alpha = 1.95*degrees 66 | axicon = Axicon(alpha, n, 100) 67 | y = 2 68 | L = y/tan(axicon.deviationAngle()) 69 | self.assertAlmostEqual(axicon.focalLineLength(y), L, 1) 70 | 71 | def testHighRayIsDeviatedDown(self): 72 | ray = Ray(10, 0) 73 | n = 1.1 74 | alpha = 2.56*degrees 75 | axicon = Axicon(alpha, n, 50) 76 | outputRay = axicon*ray 77 | self.assertEqual(outputRay.theta, -axicon.deviationAngle()) 78 | self.assertTrue(outputRay.theta < 0) 79 | 80 | def testLowRayIsDeviatedUp(self): 81 | ray = Ray(-10, 0) 82 | n = 1.1 83 | alpha = 2.56*degrees 84 | axicon = Axicon(alpha, n, 50) 85 | outputRay = axicon*ray 86 | self.assertEqual(outputRay.theta, axicon.deviationAngle()) 87 | self.assertTrue(outputRay.theta > 0) 88 | 89 | @envtest.expectedFailure 90 | def testMulMatrix(self): 91 | matrix = Matrix() 92 | axicon = Axicon(2.6543, 1.2*degrees) 93 | with self.assertRaises(TypeError): 94 | axicon.mul_matrix(matrix) 95 | 96 | @envtest.expectedFailure 97 | def testDifferentMultiplications(self): 98 | ray = Ray() 99 | beam = GaussianBeam(w=1, R=10, n=1.67) 100 | matrix = Matrix() 101 | axicon = Axicon(4.3, 1.67*degrees) 102 | self.assertIsNotNone(axicon * ray) 103 | with self.assertRaises(TypeError): 104 | axicon * beam 105 | 106 | with self.assertRaises(TypeError): 107 | axicon * matrix 108 | 109 | if __name__ == '__main__': 110 | envtest.main() 111 | -------------------------------------------------------------------------------- /raytracing/tests/testsCallOtherScript.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | import sys 3 | import subprocess 4 | import os 5 | 6 | 7 | class TestCallScript(envtest.RaytracingTestCase): 8 | def setUp(self): 9 | super().setUp() 10 | self.exec = sys.executable 11 | self.encoding = sys.stdout.encoding 12 | self.emptyFile = self.tempFilePath('script.py') 13 | open(self.emptyFile, "w").close() 14 | self.assertTrue(os.path.exists(self.emptyFile)) 15 | 16 | self.printHelloWorld = self.tempFilePath('helloWorld.py') 17 | with open(self.printHelloWorld, "w") as helloWorld: 18 | helloWorld.write('''print("hello world")''') 19 | 20 | def testCallFileNotFound(self): 21 | file = " fileDoesNotExist.py" # Leading space important 22 | 23 | processReturn = subprocess.run([self.exec, file], capture_output=True) 24 | self.assertEqual(processReturn.returncode, 2) 25 | 26 | def testCallScript(self): 27 | processReturn = subprocess.run([self.exec, self.emptyFile]) 28 | self.assertEqual(processReturn.returncode, 0) 29 | 30 | def testGetWhatIsPrinted(self): 31 | processCompleted = subprocess.run([self.exec, self.printHelloWorld], capture_output=True) 32 | printed = processCompleted.stdout.decode(self.encoding) 33 | self.assertEqual(processCompleted.stdout.strip(), b"hello world") 34 | 35 | def testGetWhatIsPrintedWithChildProcesses(self): 36 | code = """ 37 | import multiprocessing 38 | 39 | def printHelloName(name): 40 | print(f"Hello {name}") 41 | 42 | if __name__ == "__main__": 43 | inputNames = ["Toto", "Toto Jr."] 44 | processes = 4 45 | with multiprocessing.Pool(processes=processes) as pool: 46 | outputValues = pool.map(printHelloName, inputNames) 47 | """.strip() 48 | with open(self.printHelloWorld, "w") as f: 49 | f.write(code) 50 | processCompleted = subprocess.run([self.exec, self.printHelloWorld], capture_output=True, universal_newlines=True) 51 | 52 | output = processCompleted.stdout 53 | possibility1 = "Hello Toto\nHello Toto Jr.\n" 54 | possibility2 = "Hello Toto Jr.\nHello Toto\n" 55 | self.assertTrue(output == possibility1 or output == possibility2) 56 | 57 | 58 | if __name__ == '__main__': 59 | envtest.main() 60 | -------------------------------------------------------------------------------- /raytracing/tests/testsComponents.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | 4 | inf = float("+inf") 5 | 6 | 7 | class Test4fSystem(envtest.RaytracingTestCase): 8 | 9 | def test4fSystem(self): 10 | elements = [Space(10), Lens(10), Space(15), Lens(5), Space(5)] 11 | mg = MatrixGroup(elements, label="4f system") 12 | system = System4f(10, 5, label="4f system") 13 | self.assertEqual(system.A, -0.5) 14 | self.assertEqual(system.B, 0) 15 | self.assertEqual(system.C, 0) 16 | self.assertEqual(system.D, -2) 17 | self.assertEqual(system.L, 30) 18 | self.assertEqual(mg.backIndex, system.backIndex) 19 | self.assertEqual(mg.frontIndex, system.frontIndex) 20 | self.assertEqual(mg.backVertex, system.backVertex) 21 | self.assertEqual(mg.frontVertex, system.frontVertex) 22 | self.assertEqual(mg.label, system.label) 23 | self.assertTrue(system.isImaging) 24 | 25 | def test2fSystem(self): 26 | elements = [Space(10), Lens(10), Space(10)] 27 | mg = MatrixGroup(elements, label="2f system") 28 | system = System2f(10, label="2f system") 29 | self.assertEqual(system.A, 0) 30 | self.assertEqual(system.B, 10) 31 | self.assertEqual(system.C, -1 / 10) 32 | self.assertEqual(system.D, 0) 33 | self.assertEqual(system.L, 20) 34 | self.assertEqual(mg.backIndex, system.backIndex) 35 | self.assertEqual(mg.frontIndex, system.frontIndex) 36 | self.assertEqual(mg.backVertex, system.backVertex) 37 | self.assertEqual(mg.frontVertex, system.frontVertex) 38 | self.assertEqual(mg.label, system.label) 39 | self.assertFalse(system.isImaging) 40 | 41 | def test4fIsTwo2f(self): 42 | f1, f2 = 10, 12 43 | system4f = System4f(f1=10, f2=12) 44 | system2f_1 = System2f(f1) 45 | system2f_2 = System2f(f2) 46 | composed4fSystem = MatrixGroup(system2f_1.elements + system2f_2.elements) 47 | self.assertEqual(composed4fSystem.A, system4f.A) 48 | self.assertEqual(composed4fSystem.B, system4f.B) 49 | self.assertEqual(composed4fSystem.C, system4f.C) 50 | self.assertEqual(composed4fSystem.D, system4f.D) 51 | self.assertEqual(composed4fSystem.L, system4f.L) 52 | self.assertEqual(composed4fSystem.backIndex, system4f.backIndex) 53 | self.assertEqual(composed4fSystem.frontIndex, system4f.frontIndex) 54 | self.assertEqual(composed4fSystem.backVertex, system4f.backVertex) 55 | self.assertEqual(composed4fSystem.frontVertex, system4f.frontVertex) 56 | 57 | 58 | if __name__ == '__main__': 59 | envtest.main() 60 | -------------------------------------------------------------------------------- /raytracing/tests/testsDecorators.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | from raytracing.utils import deprecated 3 | 4 | 5 | def simpleDecoratorReturnsTuple(func): 6 | def wrapper(*args, **kwargs): 7 | return func(*args, **kwargs), func(*args, **kwargs) 8 | 9 | return wrapper 10 | 11 | 12 | def decoratorWithOneArgument(arg): 13 | def simpleDecoratorReturnsTuple(func): 14 | def wrapper(*args, **kwargs): 15 | return arg, func(*args, **kwargs) 16 | 17 | return wrapper 18 | 19 | return simpleDecoratorReturnsTuple 20 | 21 | 22 | class TestDecorators(envtest.RaytracingTestCase): 23 | 24 | def testSimpleDecorator(self): 25 | @simpleDecoratorReturnsTuple 26 | def toto(x): 27 | return x * x 28 | 29 | self.assertTupleEqual(toto(2), (4, 4)) 30 | 31 | def testDecoratorWithOneArgument(self): 32 | @decoratorWithOneArgument(0) 33 | def toto(a, b, c): 34 | return a, b, c 35 | 36 | self.assertTupleEqual(toto(1, 2, 3), (0, (1, 2, 3))) 37 | 38 | 39 | class TestDeprecatedDecorator(envtest.RaytracingTestCase): 40 | 41 | def testDeprecated(self): 42 | reason = "This is deprecated because it is a test." 43 | 44 | @deprecated(reason) 45 | def deprecatedFunction(): 46 | return "This is deprecated" 47 | 48 | with self.assertWarns(DeprecationWarning) as deprec: 49 | retVal = deprecatedFunction() 50 | 51 | self.assertEqual(retVal, "This is deprecated") 52 | self.assertEqual(str(deprec.warning), reason) 53 | -------------------------------------------------------------------------------- /raytracing/tests/testsDocTestModule.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | import doctest 3 | import raytracing 4 | from unittest.mock import Mock, patch 5 | 6 | import io 7 | import contextlib 8 | 9 | ## if a new python file is added to the module, please add it in a new line 10 | 11 | ferr = io.StringIO() 12 | fout = io.StringIO() 13 | contextlib.redirect_stderr(ferr) 14 | contextlib.redirect_stdout(fout) 15 | 16 | with patch('matplotlib.pyplot.show', new=Mock()): 17 | doctest.testmod(m=raytracing.axicon,verbose=False) 18 | doctest.testmod(m=raytracing.components,verbose=False) 19 | doctest.testmod(m=raytracing.eo,verbose=False) 20 | doctest.testmod(m=raytracing.figure,verbose=False) 21 | doctest.testmod(m=raytracing.gaussianbeam,verbose=False) 22 | doctest.testmod(m=raytracing.imagingpath,verbose=False) 23 | doctest.testmod(m=raytracing.lasercavity,verbose=False) 24 | doctest.testmod(m=raytracing.laserpath,verbose=False) 25 | doctest.testmod(m=raytracing.materials,verbose=False) 26 | doctest.testmod(m=raytracing.matrix,verbose=False) 27 | doctest.testmod(m=raytracing.matrixgroup,verbose=False) 28 | doctest.testmod(m=raytracing.ray,verbose=False) 29 | doctest.testmod(m=raytracing.rays,verbose=False) 30 | doctest.testmod(m=raytracing.specialtylenses,verbose=False) 31 | doctest.testmod(m=raytracing.utils,verbose=False) 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /raytracing/tests/testsExamples.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | import subprocess 3 | import matplotlib as mpl 4 | mpl.use('Agg') 5 | from matplotlib import patches, transforms 6 | from unittest.mock import Mock, patch 7 | 8 | from raytracing import * 9 | 10 | class TestExamples(envtest.RaytracingTestCase): 11 | 12 | def testRegex(self): 13 | pattern = r'^(ex\d+|fig.+)\.py$' 14 | matchObj = re.match(pattern, "fig8-bla.py") 15 | self.assertIsNotNone(matchObj) 16 | self.assertIsNotNone(matchObj.group(1) == 'fig8-bla') 17 | matchObj = re.match(pattern, "ex08.py") 18 | self.assertIsNotNone(matchObj) 19 | self.assertIsNotNone(matchObj.group(1) == 'ex08') 20 | 21 | def testExamplesArePresent(self): 22 | import raytracing.examples as ex 23 | self.assertTrue(len(ex.short) > 0) 24 | 25 | @envtest.skipUnless(envtest.performanceTests, "Skipping long performance tests") 26 | @patch('matplotlib.pyplot.show', new=Mock()) 27 | def testExamplesRun(self): 28 | import raytracing.examples as ex 29 | for ex in ex.short: 30 | self.assertTrue(len(ex["title"])!=0) 31 | self.assertTrue(len(ex["sourceCode"])!=0) 32 | print(".", end='', file=sys.stderr) 33 | print(ex["name"], end='', file=sys.stderr) 34 | with envtest.redirect_stdout(self.stdout): 35 | ex["code"]() 36 | 37 | def testExamplesHaveSrcCode(self): 38 | import raytracing.examples as ex 39 | for ex in ex.short: 40 | self.assertTrue(len(ex["sourceCode"])!=0) 41 | 42 | def testExamplesHaveBmpSrcCode(self): 43 | import raytracing.examples as ex 44 | for ex in ex.short: 45 | self.assertIsNotNone(ex["bmpSourceCode"]) 46 | 47 | @envtest.skipUnless(envtest.performanceTests, "Skipping long performance tests") 48 | def testScriptsRun(self): 49 | import raytracing.examples as ex 50 | for scripts in ex.long: 51 | err = subprocess.run([sys.executable, scripts["path"]], capture_output=True) 52 | self.assertTrue(err == 0) 53 | 54 | 55 | 56 | if __name__ == '__main__': 57 | envtest.main() 58 | -------------------------------------------------------------------------------- /raytracing/tests/testsGaussian.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | 4 | inf = float("+inf") 5 | 6 | 7 | class TestBeam(envtest.RaytracingTestCase): 8 | def testBeam(self): 9 | beam = GaussianBeam(w=1) 10 | self.assertEqual(beam.w, 1) 11 | self.assertEqual(beam.R, float("+Inf")) 12 | self.assertEqual(beam.wavelength, 0.0006328) 13 | 14 | def testBeamWAndQGiven(self): 15 | w = 1 16 | q = 4.96459e3 * 1j 17 | self.assertDoesNotRaise(GaussianBeam, ValueError, q=q, w=w) 18 | 19 | def testBeamWAndQGivenMismatch(self): 20 | w = 1 21 | q = 4.96459e3 * 1j 22 | q += q * 0.007 23 | with self.assertRaises(ValueError): 24 | GaussianBeam(q, w) 25 | 26 | def testIsInFinite(self): 27 | beam = GaussianBeam(w=inf, R=2) 28 | self.assertFalse(beam.isFinite) 29 | 30 | def testIsFinite(self): 31 | beam = GaussianBeam(w=0.1, R=20) 32 | self.assertTrue(beam.isFinite) 33 | 34 | def testFiniteW(self): 35 | beam = GaussianBeam(w=0.1) 36 | self.assertAlmostEqual(beam.w, 0.1) 37 | 38 | def testInfiniteW(self): 39 | beam = GaussianBeam(w=inf, R=10) 40 | self.assertEqual(beam.w, inf) 41 | 42 | def testNull(self): 43 | beam = GaussianBeam(0) 44 | self.assertFalse(beam.isFinite) 45 | self.assertEqual(beam.w, float("+Inf")) 46 | self.assertEqual(beam.R, float("+Inf")) 47 | self.assertEqual(beam.wavelength, 0.0006328) 48 | 49 | def testZ0is0(self): 50 | beam = GaussianBeam(w=inf, R=1) 51 | self.assertEqual(beam.zo, 0) 52 | 53 | def testZ0isNot0(self): 54 | wavelength = 632.8e-6 55 | beam = GaussianBeam(w=1, R=1, wavelength=wavelength) 56 | a = wavelength / pi 57 | imag = a / (1 + a ** 2) 58 | self.assertAlmostEqual(beam.zo, imag) 59 | 60 | def testWo(self): 61 | beam = GaussianBeam(w=1) 62 | self.assertAlmostEqual(beam.w, beam.wo, 3) 63 | self.assertAlmostEqual(beam.w, beam.waist, 3) 64 | 65 | def testWoIsNone(self): 66 | beam = GaussianBeam(w=inf, R=1) 67 | self.assertIsNone(beam.wo) 68 | 69 | def testInvalidParameters(self): 70 | with self.assertRaises(ValueError) as context: 71 | beam = GaussianBeam() 72 | self.assertEqual(str(context.exception), "Please specify 'q' or 'w'.") 73 | 74 | with self.assertRaises(Exception) as context: 75 | beam = GaussianBeam(w=1, R=0) 76 | 77 | def testMultiplicationBeam(self): 78 | # No default parameters 79 | beamIn = GaussianBeam(w=0.01, R=1, n=1.5, wavelength=0.400e-3) 80 | beamOut = Space(d=0, n=1.5) * beamIn 81 | self.assertEqual(beamOut.q, beamIn.q) 82 | self.assertEqual(beamOut.w, beamIn.w) 83 | self.assertEqual(beamOut.R, beamIn.R) 84 | self.assertEqual(beamOut.n, beamIn.n) 85 | self.assertEqual(beamOut.wavelength, beamIn.wavelength) 86 | 87 | def testDielectricInterfaceBeam(self): 88 | # No default parameters 89 | beamIn = GaussianBeam(w=10, R=inf, n=1.5, wavelength=0.400e-3) 90 | beamOut = DielectricInterface(R=-20, n1=1.5, n2=1.0) * beamIn 91 | self.assertEqual(beamOut.w, beamIn.w) 92 | self.assertEqual(beamOut.n, 1.0) 93 | self.assertEqual(beamOut.wavelength, beamIn.wavelength) 94 | 95 | def testPointBeam(self): 96 | beamIn = GaussianBeam(w=0.0000001) 97 | beamOut = Space(d=100) * beamIn 98 | self.assertEqual(beamOut.R, 100) 99 | self.assertEqual(beamOut.zo, beamIn.zo) 100 | self.assertEqual(beamOut.confocalParameter, beamIn.confocalParameter) 101 | self.assertEqual(beamOut.rayleighRange, beamIn.rayleighRange) 102 | 103 | def testFocalSpot(self): 104 | beamIn = GaussianBeam(w=0.1) 105 | beamOut = Space(d=100) * beamIn 106 | self.assertEqual(beamOut.waistPosition, -100) 107 | 108 | def testStr(self): 109 | self.assertIsNotNone(GaussianBeam(w=0.1).__str__) 110 | self.assertTrue(len(GaussianBeam(w=0.1).__str__()) != 0) 111 | 112 | def testStrInvalidRadiusOfCurvature(self): 113 | beam = GaussianBeam(w=inf, R=1) 114 | self.assertFalse(beam.isFinite) 115 | self.assertEqual(str(beam), "Beam is not finite: q=(1+0j)") 116 | 117 | def testPerformance(self): 118 | path = LaserPath() 119 | path.append(Space(d=100)) 120 | beamIn = GaussianBeam(w=0.01, R=1, wavelength=0.400e-3) 121 | 122 | path.trace(beamIn) 123 | 124 | 125 | if __name__ == '__main__': 126 | envtest.main() 127 | -------------------------------------------------------------------------------- /raytracing/tests/testsLagrangeInvariantIdeas.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | import numpy as np 4 | 5 | def distHist(rays): 6 | localRays = list(rays) 7 | lagrange = [] 8 | while len(localRays) >= 2: 9 | ray1 = localRays.pop() 10 | ray2 = localRays.pop() 11 | lagrange.append(ray1.y*ray2.theta - ray1.theta*ray2.y) 12 | showHistogram(lagrange) 13 | 14 | def histogramValues(values): 15 | counts, binEdges = histogram(values, bins=40, density=True) 16 | ys = list(counts) 17 | xs = [] 18 | for i in range(len(binEdges) - 1): 19 | xs.append((binEdges[i] + binEdges[i + 1]) / 2) 20 | return xs, ys 21 | 22 | def showHistogram(values,title=""): 23 | fig, axis1 = plt.subplots(1) 24 | fig.tight_layout(pad=3.0) 25 | xs, ys = histogramValues(values) 26 | axis1.set_title(title) 27 | axis1.plot(xs, (ys), 'ko') 28 | axis1.set_xlabel("Values") 29 | axis1.set_ylabel("Count") 30 | plt.show() 31 | 32 | def printTrace(trace): 33 | for r in trace: 34 | print(r.y, r.theta, r.z, r.isBlocked) 35 | print() 36 | 37 | def rayTraceFromCalculation(ray, principalTrace, axialTrace): 38 | principal = principalTrace[0] 39 | axial = axialTrace[0] 40 | 41 | I12 = principal.theta * axial.y - axial.theta * principal.y 42 | I13 = principal.theta * ray.y - ray.theta * principal.y 43 | I32 = ray.theta * axial.y - axial.theta * ray.y 44 | A = I32/I12 45 | B = I13/I12 46 | trace = [] 47 | # print(A,B) 48 | for i in range(len(principalTrace)): 49 | r1 = principalTrace[i] 50 | r2 = axialTrace[i] 51 | r3 = Ray(y=A*r1.y + B*r2.y, theta = A*r1.theta + B*r2.theta) 52 | r3.z = r1.z 53 | trace.append(r3) 54 | 55 | return trace 56 | 57 | class TestLagrange(envtest.RaytracingTestCase): 58 | @envtest.skip 59 | def testLagrange(self): 60 | path = ImagingPath(label="4f system") 61 | path.objectHeight = 50 62 | path.append(System4f(f1=30, diameter1=60, f2=40, diameter2=100)) 63 | path.append(Aperture(diameter=10, label='Camera')) 64 | # path.display() 65 | path.reportEfficiency(nRays=10000) 66 | 67 | @envtest.redirectStdOutToFile 68 | @envtest.patch('matplotlib.pyplot.show', new=envtest.Mock()) 69 | def testObjective(self): 70 | path = ImagingPath(label="Objective") 71 | path.append(Aperture(diameter=22.5)) 72 | path.append(Space(d=180)) 73 | path.append(Lens(f=180, diameter=50, label='Tube lens')) 74 | path.append(Space(d=180)) 75 | path.append(olympus.XLUMPlanFLN20X()) 76 | path.flipOrientation() 77 | self.assertAlmostEqual( abs(path.magnification()[0]), 20) 78 | self.assertAlmostEqual( path.imageSize(), 22.5,3) 79 | path.reportEfficiency(nRays=10000) 80 | path.display(rays=ObjectRays(diameter=10)) 81 | 82 | if __name__ == '__main__': 83 | envtest.main() 84 | -------------------------------------------------------------------------------- /raytracing/tests/testsLaserPath.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | 4 | inf = float("+inf") 5 | 6 | class TestLaserPath(envtest.RaytracingTestCase): 7 | 8 | def testLaserPathNoElements(self): 9 | lasPath = LaserPath() 10 | self.assertIsNone(lasPath.inputBeam) 11 | self.assertTrue(lasPath.showElementLabels) 12 | self.assertTrue(lasPath.showPointsOfInterest) 13 | self.assertTrue(lasPath.showPointsOfInterestLabels) 14 | self.assertTrue(lasPath.showPlanesAcrossPointsOfInterest) 15 | self.assertListEqual(lasPath.elements, []) 16 | 17 | def testLaserPath(self): 18 | elements = [Space(5), Lens(5), Space(20), Lens(15), Space(15)] 19 | lp = LaserPath(elements, "Laser Path") 20 | self.assertListEqual(lp.elements, elements) 21 | self.assertEqual(lp.label, "Laser Path") 22 | 23 | def testLaserPathIncorrectElements(self): 24 | elements = [Ray(), Lens(10)] 25 | with self.assertRaises(TypeError): 26 | LaserPath(elements) 27 | 28 | @envtest.skip("This test needs to be moved to Figure") 29 | def testRearrangeBeamTraceForPlotting(self): 30 | x = [x for x in range(1, 6)] 31 | y = [y for y in range(1, 6)] 32 | rayList = [GaussianBeam(w=x_, z=y_) for (x_, y_) in zip(x, y)] 33 | lp = LaserPath() 34 | self.assertTupleEqual(lp.rearrangeBeamTraceForPlotting(rayList), (x, y)) 35 | 36 | 37 | class TestLaserCavity(envtest.RaytracingTestCase): 38 | 39 | 40 | def testEigenModesNoPower(self): 41 | lp = LaserCavity([Space(10)]) 42 | self.assertTupleEqual(lp.eigenModes(), (None, None)) 43 | 44 | def testEigenModes(self): 45 | lp = LaserCavity([Space(10), Lens(10)]) 46 | beam1, beam2 = lp.eigenModes() 47 | self.assertEqual(beam1.q.real, -5) 48 | self.assertEqual(beam2.q.real, -5) 49 | self.assertAlmostEqual(beam1.q.imag, -5 * 3 ** 0.5) 50 | self.assertAlmostEqual(beam2.q.imag, 5 * 3 ** 0.5) 51 | 52 | def testEigenModesQIs0(self): 53 | lp = LaserCavity([Lens(10)]) 54 | beam1, beam2 = lp.eigenModes() 55 | self.assertEqual(beam1.q, beam2.q) 56 | self.assertEqual(beam1.q, 0) 57 | 58 | def testLaserModesNoPower(self): 59 | lp = LaserCavity([Space(10)]) 60 | self.assertListEqual(lp.laserModes(), []) 61 | 62 | def testLaserModesOneModeQ1isNone(self): 63 | lp = LaserCavity([Space(10), Lens(10)]) 64 | laserModes = lp.laserModes() 65 | beam = laserModes[0] 66 | self.assertEqual(len(laserModes), 1) 67 | self.assertEqual(beam.q.real, -5) 68 | self.assertAlmostEqual(beam.q.imag, 5 * 3 ** 0.5) 69 | 70 | def testLaserModesOneModeQ2isNone(self): 71 | elements = [Space(1, 1.33), DielectricInterface(1.33, 1, 1), ThickLens(1.33, -10, -5, -20)] 72 | lp = LaserCavity(elements) 73 | laserModes = lp.laserModes() 74 | beam = laserModes[0] 75 | self.assertEqual(len(laserModes), 1) 76 | self.assertAlmostEqual(beam.q.real, -5.90770102) 77 | self.assertAlmostEqual(beam.q.imag, 1.52036515) 78 | 79 | def testLaserModesNoModeInfineElements(self): 80 | lp = LaserCavity([Space(10), CurvedMirror(5)]) 81 | self.assertListEqual(lp.laserModes(), []) 82 | 83 | def testLaserModesNoModeNoElement(self): 84 | lp = LaserCavity() 85 | self.assertListEqual(lp.laserModes(), []) 86 | 87 | def testLaserUnstableCavity(self): 88 | laser = LaserCavity() 89 | laser.append(Space(d=10)) 90 | laser.append(Lens(f=5)) 91 | laser.append(Space(d=10)) 92 | 93 | self.assertIsNotNone(laser) 94 | self.assertTrue(len(laser.laserModes()) == 0) 95 | 96 | def testLaserStableCavity(self): 97 | laser = LaserCavity() 98 | laser.append(Space(d=10)) 99 | laser.append(Lens(f=20)) 100 | laser.append(Space(d=10)) 101 | 102 | self.assertIsNotNone(laser) 103 | self.assertTrue(len(laser.laserModes()) == 1) 104 | 105 | if __name__ == '__main__': 106 | envtest.main() 107 | -------------------------------------------------------------------------------- /raytracing/tests/testsLimits.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | 4 | inf = float("+inf") 5 | 6 | 7 | class TestLimits(envtest.RaytracingTestCase): 8 | def testLimitsThatExist(self): 9 | # Limits that exist 10 | self.assertEqual(inf, float('+inf')) 11 | self.assertEqual(0 / inf, 0) 12 | self.assertEqual(inf * inf, inf) 13 | self.assertEqual(inf * 10, inf) 14 | self.assertEqual(inf * (-10), -inf) 15 | self.assertEqual(inf + 10, inf) 16 | self.assertEqual(inf * (-inf), -inf) 17 | self.assertEqual((-inf) * (-inf), inf) 18 | self.assertEqual((inf) ** (inf), inf) 19 | 20 | # Dividing by zero is never allowed 21 | with self.assertRaises(Exception) as context: 22 | inf / 0 23 | 24 | def testLimitsThatDontExist(self): 25 | self.assertTrue(isnan(inf - inf)) 26 | self.assertTrue(isnan(inf * 0)) 27 | 28 | def testLimitsThatExistButPythonDoesNotConsider(self): 29 | self.assertEqual((1.0) ** (inf), 1) 30 | 31 | # This rounds off to 0.99999 on my computer and the test fails 32 | # a = 3.3 33 | # b = 0.1 34 | # c = 3.0 35 | # self.assertEqual(((a/c-b))**(inf), 1) 36 | 37 | 38 | if __name__ == '__main__': 39 | envtest.main() 40 | -------------------------------------------------------------------------------- /raytracing/tests/testsMaterials.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | 4 | 5 | class TestMaterial(envtest.RaytracingTestCase): 6 | def testMaterialInvalid(self): 7 | self.assertRaises(TypeError, Material.n, 5) 8 | 9 | class TestMaterialSubclasses(envtest.RaytracingTestCase): 10 | def setUp(self) -> None: 11 | self.materials = Material.__subclasses__() 12 | super().setUp() 13 | 14 | def testMaterialsList(self): 15 | self.assertTrue(len(self.materials) > 10) 16 | 17 | def testMaterialsList2(self): 18 | self.assertTrue(Material.all()) 19 | 20 | def testFindMaterialByName(self): 21 | self.assertIsNotNone(Material.findByName('NSF10')) 22 | 23 | def testFindMaterialByIndex(self): 24 | match = Material.findByIndex(n=1.6, wavelength=0.5, tolerance=0.1) 25 | self.assertIsNotNone(match) 26 | self.assertTrue(len(match) > 2) 27 | 28 | match = Material.findByIndex(n=5.6, wavelength=0.5, tolerance=0.1) 29 | self.assertIsNotNone(match) 30 | self.assertTrue(len(match) == 0) 31 | 32 | def testMaterialAbbeNumber(self): 33 | for material in self.materials: 34 | self.assertIsNotNone(material().abbeNumber()) 35 | self.assertIsNotNone(material().Vd()) 36 | 37 | def testMaterialSubclassesTypeError(self): 38 | fails = [] 39 | for material in self.materials: 40 | try: 41 | self.assertRaises(TypeError, material.n, None) 42 | self.assertRaises(TypeError, material.n, 'test') 43 | except AssertionError: 44 | fails.append('TypeError for subclass {}'.format(material.__name__)) 45 | self.assertEqual([], fails) 46 | 47 | def testMaterialSubclassesValueErrors(self): 48 | fails = [] 49 | for material in self.materials: 50 | try: 51 | self.assertRaises(ValueError, material.n, 100) 52 | self.assertRaises(ValueError, material.n, 0) 53 | self.assertRaises(ValueError, material.n, -100) 54 | except AssertionError: 55 | fails.append('ValueError for subclass {}'.format(material.__name__)) 56 | self.assertEqual([], fails) 57 | 58 | @envtest.expectedFailure 59 | def testMaterialSubclasses(self): 60 | ''' These are sample values of refractive indices for each subclass of material for a wavelength of 0.6 micron. 61 | In case of a new category of material, make sure to add the sample value to the list. Indices can be found on : 62 | https://refractiveindex.info/ ''' 63 | fails = [] 64 | refractiveIndices = {'N_BK7': 1.5163, 'N_SF2': 1.6465, 'SF2': 1.6465, 'SF5': 1.6714, 'N_SF5': 1.6714, 65 | 'N_SF6HT': 1.8033, 'N_SF10': 1.7267, 'N_SF11': 1.7829, 'N_BAF10': 1.6692, 66 | 'E_BAF11': 1.6659, 'N_BAK1': 1.5719, 'N_BAK4': 1.5682, 'FK51A': 1.4862, 'LAFN7': 1.7482, 67 | 'N_LASF9': 1.8486,'N_LAK22': 1.6504, 'N_SSK5': 1.6576, 'E_FD10': 1.7267, 68 | 'FusedSilica': 1.4580,'N_SF6':1.7159, 'N_SF8':1.6876,'N_SF57':1.8445, 'Air':1.0} 69 | 70 | for material in self.materials: 71 | name = material.__name__ 72 | n = material.n(0.6) 73 | 74 | if name not in refractiveIndices: 75 | fails.append('No test value for {0}'.format(name)) 76 | elif areRelativelyNotEqual(n, refractiveIndices[name], 1e-4): 77 | fails.append('Wrong value for subclass {}, {} not a valid n value.'.format(name, n)) 78 | self.assertEqual([], fails) 79 | 80 | 81 | if __name__ == '__main__': 82 | envtest.main() 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /raytracing/tests/testsMultiprocessor.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | import itertools 4 | import matplotlib.pyplot as plt 5 | 6 | inf = float("+inf") 7 | 8 | 9 | def multiplyBy2(value) -> float: 10 | return 2 * value 11 | 12 | 13 | def mul(value1, value2) -> float: 14 | return value1 * value2 15 | 16 | 17 | class TestMultiProcessorSupport(envtest.RaytracingTestCase): 18 | @envtest.skipIf(sys.platform == 'darwin' and sys.version_info.major == 3 and sys.version_info.minor <= 7, 19 | "Endless loop on macOS") 20 | # Some information here: https://github.com/gammapy/gammapy/issues/2453 21 | def testMultiPool(self): 22 | processes = 8 23 | inputValues = [1, 2, 3, 4] 24 | with multiprocessing.Pool(processes=processes) as pool: 25 | outputValues = pool.map(multiplyBy2, inputValues) 26 | self.assertEqual(outputValues, list(map(multiplyBy2, inputValues))) 27 | 28 | @envtest.skipIf(sys.platform == 'darwin' and sys.version_info.major == 3 and sys.version_info.minor <= 7, 29 | "Endless loop on macOS") 30 | # Some information here: https://github.com/gammapy/gammapy/issues/2453 31 | def testMultiPoolWith2Args(self): 32 | processes = 4 33 | inputValues = [1, 2, 3, 4] 34 | with multiprocessing.Pool(processes=processes) as pool: 35 | outputValues = pool.starmap(mul, itertools.product(inputValues, [2])) 36 | func = lambda val1: mul(val1, 2) 37 | self.assertEqual(outputValues, list(map(func, inputValues))) 38 | 39 | class TestSingleAndMultiProcessorCalculation(envtest.RaytracingTestCase): 40 | #@envtest.skipUnless(envtest.performanceTests) 41 | # Some information here: https://github.com/gammapy/gammapy/issues/2453 42 | def testShortPath(self): 43 | path = ImagingPath() 44 | path.append(Space(d=100)) 45 | path.append(Lens(f=50, diameter=25)) 46 | 47 | import time 48 | photons = [10, 100, 1000, 3000, 10000, 30000] 49 | single = [] 50 | for N in photons: 51 | rays = UniformRays(N=N) 52 | start = time.time() 53 | path.traceManyThrough(rays, progress=False) 54 | single.append(time.time()-start) 55 | 56 | multi = [] 57 | for N in photons: 58 | rays = UniformRays(N=N) 59 | start = time.time() 60 | path.traceManyThroughInParallel(rays, progress=False) 61 | multi.append(time.time()-start) 62 | 63 | fig, axis = plt.subplots(1) 64 | axis.plot(photons, single,'k-',label="Single processor") 65 | axis.plot(photons, multi,'k-.',label="Multiprocessor") 66 | axis.set_title("Performance for short OpticalPath") 67 | axis.set_xlabel("Number of photons") 68 | axis.set_ylabel("Time [s]") 69 | axis.legend() 70 | plt.show() 71 | 72 | def testLongPath(self): 73 | path = ImagingPath() 74 | path.append(Space(d=100)) 75 | path.append(Lens(f=50, diameter=25)) 76 | path.append(Space(d=100)) 77 | path.append(Lens(f=25, diameter=25)) 78 | path.append(Space(d=100)) 79 | path.append(Space(d=100)) 80 | path.append(Lens(f=25, diameter=25)) 81 | path.append(Space(d=100)) 82 | path.append(Lens(f=25, diameter=25)) 83 | path.append(thorlabs.AC254_100_A()) 84 | 85 | import time 86 | photons = [10, 100, 1000, 3000] 87 | single = [] 88 | for N in photons: 89 | rays = UniformRays(N=N) 90 | start = time.time() 91 | path.traceManyThrough(rays, progress=False) 92 | single.append(time.time()-start) 93 | 94 | multi = [] 95 | for N in photons: 96 | rays = UniformRays(N=N) 97 | start = time.time() 98 | path.traceManyThroughInParallel(rays, progress=False) 99 | multi.append(time.time()-start) 100 | 101 | fig, axis = plt.subplots(1) 102 | axis.plot(photons, single,'k-',label="Single processor") 103 | axis.plot(photons, multi,'k-.',label="Multiprocessor") 104 | axis.set_title("Performance for Long OpticalPath") 105 | axis.set_xlabel("Number of photons") 106 | axis.set_ylabel("Time [s]") 107 | axis.legend() 108 | plt.show() 109 | 110 | if __name__ == '__main__': 111 | envtest.main() 112 | -------------------------------------------------------------------------------- /raytracing/tests/testsPreferences.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | import os 3 | from raytracing.preferences import Preferences 4 | 5 | 6 | class TestPrefs(envtest.RaytracingTestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.savedPrefs = Preferences() 10 | self.savedPrefs.readFromDisk() 11 | 12 | def tearDown(self): 13 | self.savedPrefs.writeToDisk() 14 | super().tearDown() 15 | 16 | def testInitPrefs(self): 17 | p = Preferences() 18 | self.assertIsNotNone(p) 19 | 20 | def testReset(self): 21 | p = Preferences() 22 | p.resetPreferences() 23 | self.assertTrue(os.path.exists(p.path)) 24 | self.assertFalse(p.keys()) # empty 25 | 26 | def testPathExists(self): 27 | p = Preferences() 28 | self.assertIsNotNone(p.path) 29 | self.assertTrue(os.path.exists(p.path)) 30 | 31 | def testReadPrefs(self): 32 | p = Preferences() 33 | p.readFromDisk() 34 | self.assertIsNotNone(p) 35 | 36 | def testWritePrefs(self): 37 | p = Preferences() 38 | p["test"] = 123 39 | p.writeToDisk() 40 | p.readFromDisk() 41 | self.assertTrue(p["test"] == 123) 42 | 43 | def testPrefsAsDict(self): 44 | p = Preferences() 45 | p["test"] = 345 46 | self.assertEqual(p["test"], 345) 47 | 48 | # def testPrefsAsIter(self): 49 | # p = Preferences() 50 | # p["test"] = 1 51 | # for key in p: 52 | # self.assertEqual(key, "test") 53 | 54 | def testZZPrefsIncludesKey(self): 55 | p = Preferences() 56 | p["test"] = "ouch" 57 | self.assertTrue('test' in p) 58 | 59 | def testZZZPrefsLargeDict(self): 60 | p = Preferences() 61 | p["test"] = "ouch" 62 | p["test1"] = "ouch" 63 | p["test2"] = "ouch" 64 | p["test3"] = "ouch" 65 | self.assertTrue(len(p.keys()) >= 4) 66 | 67 | if __name__ == '__main__': 68 | envtest.main() 69 | -------------------------------------------------------------------------------- /raytracing/tests/testsPreferencesRaytracing.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | from raytracing.preferences import Preferences 4 | 5 | class TestRaytracingPrefs(envtest.RaytracingTestCase): 6 | def setUp(self): 7 | super().setUp() 8 | self.savedPrefs = Preferences() 9 | self.savedPrefs.readFromDisk() 10 | 11 | def tearDown(self): 12 | self.savedPrefs.writeToDisk() 13 | super().tearDown() 14 | 15 | def testVersionCheckPrefs(self): 16 | p = Preferences() 17 | self.assertIsNotNone(p) 18 | self.assertTrue("lastVersionCheck" in p.keys()) 19 | 20 | def testSaveBeginnerMode(self): 21 | beginnerMode(saveToPrefs=True) 22 | p = Preferences() 23 | self.assertIsNotNone(p) 24 | self.assertTrue("mode" in p.keys()) 25 | self.assertEqual(p["mode"], "beginner") 26 | expertMode(saveToPrefs=False) 27 | silentMode(saveToPrefs=False) 28 | self.assertEqual(p["mode"], "beginner") 29 | 30 | def testSaveSilentMode(self): 31 | silentMode(saveToPrefs=True) 32 | p = Preferences() 33 | self.assertIsNotNone(p) 34 | self.assertTrue("mode" in p.keys()) 35 | self.assertEqual(p["mode"], "silent") 36 | expertMode(saveToPrefs=False) 37 | beginnerMode(saveToPrefs=False) 38 | self.assertEqual(p["mode"], "silent") 39 | 40 | def testSaveExpertMode(self): 41 | expertMode(saveToPrefs=True) 42 | p = Preferences() 43 | self.assertIsNotNone(p) 44 | self.assertTrue("mode" in p.keys()) 45 | self.assertEqual(p["mode"], "expert") 46 | silentMode(saveToPrefs=False) 47 | beginnerMode(saveToPrefs=False) 48 | self.assertEqual(p["mode"], "expert") 49 | 50 | def testWarningsFormat(self): 51 | beginnerMode() 52 | message = "This is a test." 53 | filename = "test.py" 54 | lineno = 10 55 | category = UserWarning 56 | warningsMessage = warningLineFormat(message, category, filename, lineno) 57 | self.assertEqual(warningsMessage, "UserWarning [in test.py]: This is a test.\n") 58 | 59 | if __name__ == '__main__': 60 | envtest.main() 61 | -------------------------------------------------------------------------------- /raytracing/tests/testsReport.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | from raytracing.zemax import ZMXReader 4 | from numpy import linspace, pi 5 | from raytracing.materials import * 6 | import io 7 | from contextlib import redirect_stdout 8 | 9 | inf = float("+inf") 10 | 11 | class TestReport(envtest.RaytracingTestCase): 12 | stdout = io.StringIO() 13 | 14 | def setUp(self): 15 | self.stdout = io.StringIO() 16 | super().setUp() 17 | 18 | def testCreationNoASNoFS(self): 19 | f = 50 20 | path = ImagingPath() 21 | path.append(Space(d=f)) 22 | path.append(Lens(f=f)) 23 | path.append(Space(d=f)) 24 | with redirect_stdout(self.stdout): 25 | path.reportEfficiency() 26 | 27 | self.assertTrue(len(self.stdout.getvalue()) < 100) 28 | position, diameter = path.fieldStop() 29 | self.assertIsNone(position) 30 | self.assertTrue(diameter == float("+inf")) 31 | 32 | @envtest.patchMatplotLib() 33 | def testCreationASNoFS(self): 34 | f = 50 35 | path = ImagingPath() 36 | path.append(Space(d=f)) 37 | path.append(Lens(f=f,diameter=10)) 38 | path.append(Space(d=f)) 39 | with redirect_stdout(self.stdout): 40 | path.reportEfficiency() 41 | 42 | self.assertTrue(len(self.stdout.getvalue()) < 100) 43 | position, diameter = path.fieldStop() 44 | self.assertIsNone(position) 45 | self.assertTrue(diameter == float("+inf")) 46 | 47 | @envtest.patchMatplotLib() 48 | def testCreationASFS(self): 49 | f = 50 50 | path = ImagingPath() 51 | path.append(Space(d=f)) 52 | path.append(Lens(f=f, diameter=6)) 53 | path.append(Space(d=f)) 54 | path.append(Lens(f=f, diameter=6)) 55 | with redirect_stdout(self.stdout): 56 | path.reportEfficiency() 57 | 58 | self.assertTrue(len(self.stdout.getvalue()) > 100) 59 | position, diameter = path.fieldStop() 60 | self.assertIsNotNone(position) 61 | self.assertTrue(diameter != float("+inf")) 62 | 63 | 64 | if __name__ == '__main__': 65 | envtest.main() 66 | -------------------------------------------------------------------------------- /raytracing/tests/testsSpecialLenses.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | 3 | from raytracing import * 4 | 5 | inf = float("+inf") 6 | 7 | 8 | class TestSpecialLenses(envtest.RaytracingTestCase): 9 | 10 | def testOlympusLens(self): 11 | self.assertIsNotNone(olympus.LUMPlanFL40X()) 12 | self.assertIsNotNone(olympus.XLUMPlanFLN20X()) 13 | self.assertIsNotNone(olympus.MVPlapo2XC()) 14 | self.assertIsNotNone(olympus.UMPLFN20XW()) 15 | 16 | def testThorlabsLenses(self): 17 | l = thorlabs.ACN254_100_A() 18 | l = thorlabs.ACN254_075_A() 19 | l = thorlabs.ACN254_050_A() 20 | l = thorlabs.ACN254_040_A() 21 | l = thorlabs.AC254_030_A() 22 | l = thorlabs.AC254_035_A() 23 | l = thorlabs.AC254_045_A() 24 | l = thorlabs.AC254_050_A() 25 | l = thorlabs.AC254_060_A() 26 | l = thorlabs.AC254_075_A() 27 | l = thorlabs.AC254_080_A() 28 | l = thorlabs.AC254_100_A() 29 | l = thorlabs.AC254_125_A() 30 | l = thorlabs.AC254_200_A() 31 | l = thorlabs.AC254_250_A() 32 | l = thorlabs.AC254_300_A() 33 | l = thorlabs.AC254_400_A() 34 | l = thorlabs.AC254_500_A() 35 | 36 | l = thorlabs.AC508_075_B() 37 | l = thorlabs.AC508_080_B() 38 | l = thorlabs.AC508_100_B() 39 | l = thorlabs.AC508_150_B() 40 | l = thorlabs.AC508_200_B() 41 | l = thorlabs.AC508_250_B() 42 | l = thorlabs.AC508_300_B() 43 | l = thorlabs.AC508_400_B() 44 | l = thorlabs.AC508_500_B() 45 | l = thorlabs.AC508_750_B() 46 | l = thorlabs.AC508_1000_B() 47 | 48 | def testEdmundLens(self): 49 | l = eo.PN_33_921() 50 | 51 | 52 | if __name__ == '__main__': 53 | envtest.main() 54 | -------------------------------------------------------------------------------- /raytracing/tests/testsUtils.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path # fixme: requires path to raytracing/tests 2 | from raytracing.utils import checkLatestVersion 3 | 4 | import io 5 | import contextlib 6 | 7 | class TestUtils(envtest.RaytracingTestCase): 8 | def testCheckOldVersion(self): 9 | f = io.StringIO() 10 | with contextlib.redirect_stdout(f): 11 | self.assertTrue(checkLatestVersion(currentVersion="1.3.0")) 12 | self.assertTrue(len(f.getvalue()) != 0) 13 | 14 | def testCheckNewVersion(self): 15 | f = io.StringIO() 16 | with contextlib.redirect_stdout(f): 17 | self.assertFalse(checkLatestVersion(currentVersion="1.4.0")) 18 | self.assertTrue(len(f.getvalue()) == 0) 19 | 20 | if __name__ == '__main__': 21 | envtest.main() 22 | -------------------------------------------------------------------------------- /raytracing/tests/testsWarnings.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | 4 | inf = float("+inf") 5 | 6 | 7 | class TestMatrix(envtest.RaytracingTestCase): 8 | def testMatrix(self): 9 | m = Matrix() 10 | self.assertIsNotNone(m) 11 | 12 | def testThorlabsLensesWarning(self): 13 | l = thorlabs.AC254_030_A() 14 | 15 | 16 | if __name__ == '__main__': 17 | envtest.main() 18 | -------------------------------------------------------------------------------- /raytracing/tests/testsZemax.py: -------------------------------------------------------------------------------- 1 | import envtest # modifies path 2 | from raytracing import * 3 | from raytracing.zemax import ZMXReader 4 | from numpy import linspace, pi 5 | from raytracing.materials import * 6 | 7 | inf = float("+inf") 8 | 9 | class TestZemax(envtest.RaytracingTestCase): 10 | zmx = ZMXReader(r"../specifications/AC254-100-A-Zemax(ZMX).zmx") 11 | 12 | def testCreation(self): 13 | self.assertIsNotNone(self.zmx) 14 | 15 | def testSurface0Raw(self): 16 | self.assertIsNotNone(self.zmx) 17 | surface = self.zmx.rawSurfaceInfo(index=0) 18 | self.assertTrue( surface["SURF"] == 0) 19 | self.assertAlmostEqual( float(surface["CURV"][0]), 0.0) 20 | self.assertAlmostEqual( float(surface["DISZ"][0]), float("+inf")) 21 | 22 | def testSurface1Raw(self): 23 | self.assertIsNotNone(self.zmx) 24 | surface = self.zmx.rawSurfaceInfo(index=1) 25 | self.assertTrue( surface["SURF"] == 1) 26 | self.assertAlmostEqual( float(surface["CURV"][0]), 1.593625498007969800E-002) 27 | self.assertAlmostEqual( float(surface["DISZ"][0]), 4.0) 28 | 29 | def testSurface1Info(self): 30 | self.assertIsNotNone(self.zmx) 31 | surface = self.zmx.surfaceInfo(index=1) 32 | self.assertTrue( surface.number == 1) 33 | self.assertAlmostEqual( surface.R, 62.75, 1) 34 | self.assertAlmostEqual( surface.spacing, 4.0) 35 | 36 | def testSurfaces(self): 37 | self.assertIsNotNone(self.zmx) 38 | self.assertTrue(len(self.zmx.surfaces()) == 5) 39 | 40 | def testMatrixGroupLensProperties(self): 41 | design = self.zmx.matrixGroup() 42 | self.assertIsNotNone(design) 43 | lens = thorlabs.AC254_100_A() 44 | 45 | self.assertAlmostEqual(design.effectiveFocalLengths().f1, lens.effectiveFocalLengths().f1, 3) 46 | self.assertAlmostEqual(design.effectiveFocalLengths().f2, lens.effectiveFocalLengths().f1, 3) 47 | self.assertAlmostEqual(design.backFocalLength(), lens.backFocalLength(), 3) 48 | self.assertAlmostEqual(design.frontFocalLength(), lens.frontFocalLength(), 3) 49 | self.assertAlmostEqual(design.L, lens.L, 2) 50 | 51 | def testMatrixGroup2(self): 52 | design = self.zmx.matrixGroup() 53 | self.assertIsNotNone(design) 54 | lens = thorlabs.AC254_100_A() 55 | 56 | self.assertAlmostEqual(design.effectiveFocalLengths().f1, lens.effectiveFocalLengths().f1, 3) 57 | self.assertAlmostEqual(design.effectiveFocalLengths().f2, lens.effectiveFocalLengths().f1, 3) 58 | self.assertAlmostEqual(design.backFocalLength(), lens.backFocalLength(), 3) 59 | self.assertAlmostEqual(design.frontFocalLength(), lens.frontFocalLength(), 3) 60 | 61 | def testPrescription(self): 62 | self.assertIsNotNone(self.zmx.prescription()) 63 | 64 | def testWAVM(self): 65 | self.assertTrue(self.zmx.designWavelengths() == [4.861E-1, 5.876E-1, 6.563E-1]) 66 | self.assertAlmostEqual(self.zmx.designWavelength, 0.5876) 67 | 68 | def testEdmundFile(self): 69 | zmx = ZMXReader("../specifications/zmax_49270.zmx") 70 | self.assertIsNotNone(zmx) 71 | # print(zmx.prescription()) 72 | 73 | def testEdmundFile(self): 74 | zmx = ZMXReader("../specifications/AC254-125-A-Zemax(ZMX).zmx") 75 | self.assertIsNotNone(zmx) 76 | #print(zmx.prescription()) 77 | #print(zmx.matrixGroup().effectiveFocalLengths()) 78 | 79 | if __name__ == '__main__': 80 | envtest.main() 81 | -------------------------------------------------------------------------------- /raytracing/tests/traceManyThroughInParallelNoOutput.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | 3 | from raytracing import * 4 | 5 | if __name__ == '__main__': 6 | rays = [Ray(y, y) for y in range(20_000)] 7 | m = Matrix(physicalLength=1) 8 | m.traceManyThroughInParallel(rays, processes=2, progress=False) 9 | -------------------------------------------------------------------------------- /raytracing/tests/traceManyThroughInParallelWithOutput.py: -------------------------------------------------------------------------------- 1 | import envtest 2 | 3 | from raytracing import * 4 | 5 | if __name__ == '__main__': 6 | rays = [Ray(y, y) for y in range(20_000)] 7 | m = Matrix(physicalLength=1) 8 | m.traceManyThroughInParallel(rays, processes=2, progress=True) 9 | -------------------------------------------------------------------------------- /raytracing/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/RayTracing/9609b55f16c8ce4499c85a2622619f60b7675fa3/raytracing/ui/__init__.py --------------------------------------------------------------------------------