From f4cc304953d8855e5a2ca60e65216a23a0bbeeb2 Mon Sep 17 00:00:00 2001 From: yosata Date: Thu, 28 May 2020 09:41:08 +0200 Subject: [PATCH] Initial commit --- .gitignore | 30 +- fourierOptics/__init__.py | 9 + fourierOptics/data/ELT_M1_pupil_profile.fits | Bin 0 -> 16781760 bytes fourierOptics/data/__init__.py | 0 .../data/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 189 bytes .../__pycache__/glassCatalog.cpython-37.pyc | Bin 0 -> 590 bytes fourierOptics/data/glassCatalog.py | 12 + fourierOptics/fourier.py | 440 ++++ fourierOptics/fourierOptics.py | 2095 +++++++++++++++++ fourierOptics/gaussian.py | 113 + fourierOptics/misc.py | 95 + fourierOptics/opticalTrain.py | 103 + fourierOptics/optics.py | 773 ++++++ fourierOptics/profiles.py | 73 + fourierOptics/psfFitter.py | 255 ++ fourierOptics/wavefrontErrors.py | 506 ++++ .../fourierOptics_01_diffracting_aperture.py | 735 ++++++ .../fourierOptics_025_single_lens_system.py | 729 ++++++ .../fourierOptics_02_telescope.py | 771 ++++++ .../fourierOptics_03_verification.py | 775 ++++++ .../fourierOptics_04_simple_lens_system.py | 720 ++++++ .../fourierOptics_25_single_lens_system.py | 729 ++++++ test cases (standalone)/glassCatalog.py | 12 + 23 files changed, 8948 insertions(+), 27 deletions(-) create mode 100644 fourierOptics/__init__.py create mode 100755 fourierOptics/data/ELT_M1_pupil_profile.fits create mode 100644 fourierOptics/data/__init__.py create mode 100644 fourierOptics/data/__pycache__/__init__.cpython-37.pyc create mode 100644 fourierOptics/data/__pycache__/glassCatalog.cpython-37.pyc create mode 100644 fourierOptics/data/glassCatalog.py create mode 100644 fourierOptics/fourier.py create mode 100644 fourierOptics/fourierOptics.py create mode 100644 fourierOptics/gaussian.py create mode 100644 fourierOptics/misc.py create mode 100644 fourierOptics/opticalTrain.py create mode 100644 fourierOptics/optics.py create mode 100644 fourierOptics/profiles.py create mode 100644 fourierOptics/psfFitter.py create mode 100644 fourierOptics/wavefrontErrors.py create mode 100644 test cases (standalone)/fourierOptics_01_diffracting_aperture.py create mode 100644 test cases (standalone)/fourierOptics_025_single_lens_system.py create mode 100644 test cases (standalone)/fourierOptics_02_telescope.py create mode 100644 test cases (standalone)/fourierOptics_03_verification.py create mode 100644 test cases (standalone)/fourierOptics_04_simple_lens_system.py create mode 100644 test cases (standalone)/fourierOptics_25_single_lens_system.py create mode 100644 test cases (standalone)/glassCatalog.py diff --git a/.gitignore b/.gitignore index b6e4761..cefc713 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,6 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg +.DS_Store +__pychache__/ +.ipynb_checkpoints/ MANIFEST # PyInstaller diff --git a/fourierOptics/__init__.py b/fourierOptics/__init__.py new file mode 100644 index 0000000..748f886 --- /dev/null +++ b/fourierOptics/__init__.py @@ -0,0 +1,9 @@ +import os +from .fourier import * +from .gaussian import * +from .opticalTrain import * +from .optics import * +from .profiles import * +from .psfFitter import * +from .wavefrontErrors import * +from .misc import * diff --git a/fourierOptics/data/ELT_M1_pupil_profile.fits b/fourierOptics/data/ELT_M1_pupil_profile.fits new file mode 100755 index 0000000000000000000000000000000000000000..153e55eb207264f5c685ca918c01f0651cb5729d GIT binary patch literal 16781760 zcmeFv%}&BV6b4WqWnMr9bm7K@C?-w(A;N;Vt5UQHw1(CM-`;6A25F4AadYmfncVqu z@0m`L+wsjPg!9l*({LOX`7#U7Ww9vNt18rGxK5^%P}ONMPuKIEOT%P3PG+5XhbM!W zSG=#DvUMmI;VG}H@D{)4X`P0*e3LDA;=%7?cbB$)x(cM*7UcA@5N6X)=T4i-ur60}P)M;LX zY*S}Nm6yfV{=>)7!+pH^&98UVYbxgLS;NWYMaTcr|7%{m?4C1dCqRGz0RjXF{HMTy zB0zuu0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0-x;Nd$cW8c_8{FAc_w3`iM~xlWyZPw4jJ2aQ3QnXBbg@kv2vJ zL>{8RIfwIppTmk7Mokn13{B(32N*5;_S zxp#iGscr4rRkdpERSWj!{MHzIthK6UeKqHA&YJs=uVyU*1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N 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

2Xjo%JQz2R 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 zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r b3>YwAz<>b*1`HT5U|=&43>YwAz`$XkkI+K0 literal 0 HcmV?d00001 diff --git a/fourierOptics/data/__init__.py b/fourierOptics/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fourierOptics/data/__pycache__/__init__.cpython-37.pyc b/fourierOptics/data/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7308dd841f5dda151ecb8c6e966eb432893bc5c6 GIT binary patch literal 189 zcmZ?b<>g`k0?7#_aUl9Jh=2h`Aj1KOi&=m~3PUi1CZpdg`kg1alCW1E;57#@Q-FaVNq0OH~RAd$k5!kEI8!kogA!kW&O!j{6G!jZz6 z!j-}ePpqd9a z<@P>&9O;l=RGHNC&+VCg`mP7*+dnpKv`?4y&UD``eak+b=UajB|ATw&(>JdDwOwt; z8~gM<&qC*3J*DiBE~2%ixu)%_efsat73#eCE%xb!?3-S_ICRQBowt40WiPQ6_UV%k z=M`P~_trk$>*A@s2Yy65q;F7LC3IlNC;RjSA>#|Nww3nj(~<<6ZK8pSQ{@*HYyOyJ zpYC@kW^X{@3;Xo^ruK3&UT=ghdc4xskS_pFb)Ua?PacE85b z^>>why31Rx{b%}^9nv#bsek5Qp5>s)TE*(8>*Q@7#S)z9ZxqGkW>m!Ou z_&9o)8~SN7MzJR+mL%rnr>|rvVg$wunD~{YA6lGRRIHzspI=;}?^2Xskd$AcAD&v2 ztsju#qVJfOm{VDtTC5*XS(1^T2jQ1wCMV|Tm!uY#C?qNr6y;~7CYR`^<(C#^rWW}Z r0A-8yQ-D_Kr{^RV7dt~N)hnpH#bJ}1pHiBWYR3*r8yrA_j}Za^2Fci$ literal 0 HcmV?d00001 diff --git a/fourierOptics/data/glassCatalog.py b/fourierOptics/data/glassCatalog.py new file mode 100644 index 0000000..f50d65b --- /dev/null +++ b/fourierOptics/data/glassCatalog.py @@ -0,0 +1,12 @@ +#!/usr/bin/env Python3 + +# Pyton dictionary with some glass types + +catalog = { + + 'N-BK7' : [1.039612120E+00, 6.000698670E-03, 2.317923440E-01, 2.001791440E-02, 1.010469450E+00, 1.035606530E+02 ], + 'SiO2' : [0.6961663, 0.0684043, 0.4079426, 0.1162414, 0.8974794, 9.896161 ], + 'F2' : [1.345333590E+00, 9.977438710E-03, 2.090731760E-01, 4.704507670E-02, 9.373571620E-01, 1.118867640E+02 ], + 'S-FPL51' : [1.170105050E+00, 6.162039240E-03, 4.757107830E-02, 2.633728760E-02, 7.638324450E-01, 1.418826420E+02 ], + 'S-LAH71' : [1.982800310E+00, 1.189874560E-02, 3.167584500E-01, 5.271560010E-02, 2.444726460E+00, 2.132206970E+02 ], +} \ No newline at end of file diff --git a/fourierOptics/fourier.py b/fourierOptics/fourier.py new file mode 100644 index 0000000..40a1af6 --- /dev/null +++ b/fourierOptics/fourier.py @@ -0,0 +1,440 @@ +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def plane_wave_propagation(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates=None): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(xin)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(xin)) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) # Factor to scale the intensity after the FFTs + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=_dpix) + _fy_l = np.fft.fftfreq(_m, d=_dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phase_exponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + # _y, _x = np.mgrid[-_n / 2 * _dpix : _n / 2 * _dpix : 1j*_n, -_m / 2 * _dpix : _m / 2 * _dpix : 1j * _m] + _y, _x = yin, xin + + if outputCoordinates is not None: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*_dpix, _n/2*_dpix, _n) + _y = np.linspace(-_m/2*_dpix, _m/2*_dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputCoordinates[0],outputCoordinates[1],_n) + _yi = np.linspace(outputCoordinates[2],outputCoordinates[3],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputCoordinates[0]:outputCoordinates[1]:1j*_n, outputCoordinates[2]:outputCoordinates[3]:1j * _m] + + + _E_xyz_extent = [np.min(_x.real), np.max(_x.real), np.min(_y.real), np.max(_x.real)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x.real, _y.real]) + + + def fresnel_propagation(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, + scaleInputArray=False, outputCoordinates=None, apply_interpolation=True): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beam_radius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + elif outputCoordinates is not None: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + else: + _xip = xin[0] + _yip = yin[:,0] + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + if outputCoordinates and apply_interpolation: + _f_ip_real = interpolate.interp2d(_x.real, _y.real, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x.real, _y.real, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x.max()/_xip.max())*(_y.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + elif outputCoordinates and not apply_interpolation: + _arraySelection = self.select_subsection(_x, _y, outputCoordinates) + _xip = _x.real[_arraySelection[0]:_arraySelection[1]+1] + _yip = _y.real[_arraySelection[2]:_arraySelection[3]+1] + _E_out_ip = _E_out[_arraySelection[0]:_arraySelection[1]+1,_arraySelection[2]:_arraySelection[3]+1] + _E_out_ip = np.conj(_E_out_ip) + else: + _E_out_ip = _E_out + _xip, _yip = _x, _y + + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x.real, _y.real]) + + + def freq_array(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phase_exponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussian_beam_fit(a,dz,x,y) - og_data).flatten()).real + + + def gaussian_beam_fit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + def optical_transfer_function(self, PSF, x, y, dpix=None): + ''' + Calculates the optical transfer function of the system response + Input is the image on the detector, preferably of a PSF + The frequencies are given over the whole provided range, but are + typically limited to 1 / (lambda * F/#). + ''' + if dpix is None: + dpix = np.mean(np.diff(x.real)) + if dpix < 1e-99: + dpix = np.mean(np.diff((x.real).T)) + print('dpix is {}'.format(dpix)) + # FFT input + _A_in = pyfftw.empty_aligned((PSF.shape), dtype='complex128') + _A_in[:] = PSF + + # Output of the FFT + _A_out = pyfftw.empty_aligned(PSF.shape, dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_A_in), _A_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _A_out = np.fft.fftshift(_A_out) + + # Normalise the output + _OTF = _A_out / np.max(_A_out) + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(x)[0], d=dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(y)[0], d=dpix)) + return _OTF, _fx, _fy + + + def modulation_transfer_function(self, *args): + ''' + Calculates the modulation transfer function of the system response + Input is the image on the detector, preferably of a PSF + The frequencies are given over the whole provided range, but are + typically limited to 1 / (lambda * F/#). + ''' + _OTF, _fx, _fy = self.optical_transfer_function(*args) + return np.abs(_OTF), _fx, _fy + + + def encircled_energy_function(self, PSF, x, y, x0=0, y0=0, dpix=None, originalPSF=None): + ''' + Function that returns the encircled energy as a function of distance + from the centroid position, defined by x0 and y0. + + Parameters: + PSF - Intensity map of the PSF + x,y - x and y coordinates corresponding to the PSF + x0,y0 - Optional; location of the centroid. If not given, + x0=0 and y0=0 are assumed. + dpix - Optional; pixel scale, default=1 + + Returns: + _EEF - Array with encircled energy values + _r - Array with radius values + + ''' + if dpix is None: + dpix = np.mean(np.diff(x)) + if dpix < 1e-99: + dpix = np.mean(np.diff(x.T)) + + if originalPSF is None: + _total_energy = np.sum(PSF) + else: + _total_energy = np.sum(originalPSF) + _r = np.sqrt((x.real-x0)**2 + (y.real-y0)**2) + _r_ls = np.linspace(0,_r.max(),len(_r)) + _mask = [ _r <= i for i in _r_ls] + _EEF = np.asarray([np.sum(PSF[m]) for m in _mask])/_total_energy + _r = _r_ls * dpix + return _EEF, _r + + @staticmethod + def find_nearest(array, value): + array = np.asarray(array) + diff = (np.abs(array - value)).argmin() + idx = np.unravel_index(diff, array.shape) + return idx + + def select_subsection(self, x, y , outputCoordinates): + select_1 = np.max(self.find_nearest(x, outputCoordinates[0])) + select_2 = np.max(self.find_nearest(x, outputCoordinates[1])) + select_3 = np.max(self.find_nearest(y, outputCoordinates[2])) + select_4 = np.max(self.find_nearest(y, outputCoordinates[3])) + return [select_1, select_2, select_3, select_4] diff --git a/fourierOptics/fourierOptics.py b/fourierOptics/fourierOptics.py new file mode 100644 index 0000000..0071468 --- /dev/null +++ b/fourierOptics/fourierOptics.py @@ -0,0 +1,2095 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate +import types +import time +from math import ceil +from scipy.special import factorial, j1 +import scipy.optimize as opt +from astropy.io import fits +import os + + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def plane_wave_propagation(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates=None): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(xin)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(xin)) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) # Factor to scale the intensity after the FFTs + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=_dpix) + _fy_l = np.fft.fftfreq(_m, d=_dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phase_exponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + # _y, _x = np.mgrid[-_n / 2 * _dpix : _n / 2 * _dpix : 1j*_n, -_m / 2 * _dpix : _m / 2 * _dpix : 1j * _m] + _y, _x = yin, xin + + if outputCoordinates is not None: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*_dpix, _n/2*_dpix, _n) + _y = np.linspace(-_m/2*_dpix, _m/2*_dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputCoordinates[0],outputCoordinates[1],_n) + _yi = np.linspace(outputCoordinates[2],outputCoordinates[3],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputCoordinates[0]:outputCoordinates[1]:1j*_n, outputCoordinates[2]:outputCoordinates[3]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def fresnel_propagation(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates=None): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beam_radius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + elif outputCoordinates is not None: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + else: + _xip = xin[0] + _yip = yin[:,0] + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + if outputCoordinates: + _f_ip_real = interpolate.interp2d(_x.real, _y.real, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x.real, _y.real, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x.max()/_xip.max())*(_y.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + else: + _E_out_ip = _E_out + + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freq_array(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phase_exponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussian_beam_fit(a,dz,x,y) - og_data).flatten()).real + + + def gaussian_beam_fit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + def optical_transfer_function(self, PSF, x, y, dpix): + ''' + Calculates the optical transfer function of the system response + Input is the image on the detector, preferably of a PSF + The frequencies are given over the whole provided range, but are + typically limited to 1 / (lambda * F/#). + ''' + # FFT input + _A_in = pyfftw.empty_aligned((PSF.shape), dtype='complex128') + _A_in[:] = PSF + + # Output of the FFT + _A_out = pyfftw.empty_aligned(PSF.shape, dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_A_in), _A_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _A_out = np.fft.fftshift(_A_out) + + # Normalise the output + _OTF = _A_out / np.max(_A_out) + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(x)[0], d=dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(y)[0], d=dpix)) + return _OTF, _fx, _fy + + + def modulation_transfer_function(self, *args): + ''' + Calculates the modulation transfer function of the system response + Input is the image on the detector, preferably of a PSF + The frequencies are given over the whole provided range, but are + typically limited to 1 / (lambda * F/#). + ''' + _OTF, _fx, _fy = self.optical_transfer_function(*args) + return np.abs(_OTF), _fx, _fy + + + def encircled_energy_function(self, PSF, x, y, x0=0, y0=0, dpix=1, originalPSF=None): + ''' + Function that returns the encircled energy as a function of distance + from the centroid position, defined by x0 and y0. + + Parameters: + PSF - Intensity map of the PSF + x,y - x and y coordinates corresponding to the PSF + x0,y0 - Optional; location of the centroid. If not given, + x0=0 and y0=0 are assumed. + dpix - Optional; pixel scale, default=1 + + Returns: + _EEF - Array with encircled energy values + _r - Array with radius values + + ''' + if originalPSF is None: + _total_energy = np.sum(PSF) + else: + _total_energy = np.sum(originalPSF) + _r = np.sqrt((x.real-x0)**2 + (y.real-y0)**2) + _r_ls = np.linspace(0,_r.max(),len(_r)) + _mask = [ _r <= i for i in _r_ls] + _EEF = np.asarray([np.sum(PSF[m]) for m in _mask])/_total_energy + _r = _r_ls * dpix + return _EEF, _r + + + + + +class Profiles: + def __init__(self): + pass + + def top_hat(self, r, n, m, dpix, oversample=1, dx=0, dy=0): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt((i-dx)**2 + (j-dy)**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + + def obscured_top_hat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spider_top_hat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + def gaussian(self, r, n, m, dpix, oversample=1, dx=0, dy=0): + ''' + Creates a grid of n x m points, following a gaussian function + where the radius r is the FWHM of the gaussian shape. + ''' + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt((i-dx)**2 + (j-dy)**2) + return np.exp(-((j-dx)**2 + (i-dy)**2) / r**2) + + + def ELT_pupil(self, n, m, dpix): + file_dir_path = os.path.dirname(os.path.realpath(__file__)) + fits_image_filename = file_dir_path + '/data/ELT_M1_pupil_profile.fits' + _ELT_pupil = fits.open(fits_image_filename)[0].data + _ELT_inner_diam = 38542 # all glass diameter of M1 in mm + _dpix_fits = _ELT_inner_diam / 623 # pixels on the diameter filled with 'glass' + _resx, _resy = np.shape(_ELT_pupil) + _xf = np.linspace(-_resx/2*_dpix_fits, _resx/2*_dpix_fits, _resx) + _yf = np.linspace(-_resy/2*_dpix_fits, _resy/2*_dpix_fits, _resy) + _f_pupil = interpolate.interp2d(_xf, _yf, _ELT_pupil, kind='linear') + _x = np.linspace(-n/2*dpix, n/2*dpix, n) + _y = np.linspace(-m/2*dpix, m/2*dpix, m) + _ELT_pupil = _f_pupil(_x, _y) + return _ELT_pupil + + + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + + def fundamental_mode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beam_radius(z) + _R = self.curvature(z) + _phi0 = self.phase_shift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + + def curvature(self, z): + return z + self.z_c **2 / z + + + def beam_radius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + + def phase_shift(self, z): + return np.arctan(z/self.z_c) + + + def relative_power(self, r, z): + return np.exp(-2 * (r / self.beam_radius(z))**2) + + + + + +class GaussianBeamPropagation(GaussianBeam): + def __init__(self, gaussianBeamObject): + self.inputBeam = gaussianBeamObject + self.l = self.inputBeam.l + self.z_c = self.inputBeam.z_c + self.theta_0 = self.inputBeam.theta_0 + self.w0 = self.inputBeam.w0 + self.fwhm = self.inputBeam.fwhm + self.beam_width = self.w0 + + + def propagate_with_a_lens(self, ABCD, d_out, d_in=0, rmax=None): + ''' + Propagates the gaussian beam through a lens with the ray transfer matrix ABCD. + This follows the equations of Ch. 3.3 of Goldsmith's Quasi-optical Systems + + Parameters: + ABCD - Ray transfer matrix, use ABCD_paraxial_lens to calculate + this matrix for a paraxial lens, with focal length f. + d_out - Distance after the lens over which the beam is propagated. + d_in - Distance from the beam waist (w0) to the lens. Default is 0. + rmax - Maximum radius for which to calculate the beam profile. + If None, it will be set to three times the beam width at + the output distance. + + Output: + The beam information. Can be plotted using imshow() + The beam width at the given point can be retrieved by using self.beam_width + + ''' + _A,_B,_C,_D = ABCD + _focalDistance = - ( (_A * d_in + _B) * (_C * d_in + _D) + _A * _C * self.z_c**2 ) / ( (_C * d_in + _D)**2 + _C**2 * self.z_c**2 ) + _z = d_out - _focalDistance + + if d_out > _focalDistance: + d_l = d_out - _focalDistance + d_out = _focalDistance + _w0_out = self.w0 / np.sqrt((_C * d_in + _D)**2 + _C**2 * self.z_c**2) + _z_c_new = np.pi * _w0_out**2 / self.l + _wl_out = _w0_out * np.sqrt(1 + (d_l/_z_c_new)**2) + + # _q_out = ((_A + _C * d_out )*1j*self.z_c + (_A + _C * d_out)*d_in + (_B + _D*d_out)) / (_C * 1j * self.z_c + _C * d_in + _D) + _w0_out = self.w0 / np.sqrt((_C * d_in + _D)**2 + _C**2 * self.z_c**2) + + self.z_c = np.pi * _w0_out**2 / self.l + self.w0 = _w0_out + self.theta_0 = self.l / (np.pi * _w0_out) + self.fwhm = 1.18 * self.theta_0 + self.beam_width = self.beam_radius(_z) + + if rmax is None: + rmax = 3 * self.beam_width + + _y, _x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + _r = np.sqrt(_x**2 + _y**2) + if _z != 0: + _w = self.beam_radius(_z) + _R = self.curvature(_z) + _phi0 = self.phase_shift(_z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + + def ABCD_paraxial_lens(self, f): + return [1, 0, -1/f, 1] + + + + + +class LensObject: + def __init__(self, CA, name=None): + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + self.CA = CA + self.type = None + self.f = None + self.sag = None + self.wavefrontError = False + + def add_wavefront_error(self, z): + assert len(z.shape)==2, 'Wavefront data is not 2D. Cannot continue' + self.sag = z + self.wavefrontError = True + + + + + +class ParaxialLens(LensObject): + def __init__(self, f, CA, name=None): + super().__init__(CA, name) + self.f = f + self.type = 'PARAXIAL' + + + def find_focal_length(self): + return self.f + + + def phase_delay(self, x, y, l, **kwargs): + _k = 2 * np.pi / l + _t_l = np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + if self.wavefrontError: + _t_l *= np.exp(-1j * _k * self.sag / l) + + # Masking out the parts that fall outside the lens clear aperture + # dist_from_center = np.sqrt(x**2 + y**2) + # _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + # _t_l *= _lensMask + return _t_l + + + + + +class SphericalLens(LensObject): + ''' + The SphericalLens object describes any spherical lens and + finds the phase delay for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, thickness, R1, R2, CA, name=None): + super().__init__(CA, name) + assert glassCatalog.catalog.get(glass), 'Glass type not in catalog' + self.type = 'SPHERICAL' + self.thickness = thickness # mm + self.R1 = R1 # mm + self.R2 = R2 # mm + self.glass = glass + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35e-3 # mm + self.f = self.find_focal_length() + + + def set_first_surface_sag(self, x,y): + ''' + Defines the shape of the first surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _first_surface_sag = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _first_surface_sag *= _lensMask + self.first_surface_sag = _first_surface_sag + return self.first_surface_sag + + + def set_second_surface_sag(self, x,y): + ''' + Defines the shape of the second surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _second_surface_sag = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _second_surface_sag *= _lensMask + self.second_surface_sag = _second_surface_sag + return self.second_surface_sag + + + def get_first_surface_sag(self): + return self.first_surface_sag + + + def get_second_surface_sag(self): + return self.second_surface_sag + + + def find_refractive_index(self, l): + # l in mm needs to be converted to l in micron + l *= 1e3 + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* l**2)/(l**2 - _a[1]) + (_a[2] * l**2) / (l**2 - _a[3]) + (_a[4] * l**2) / (l**2 - _a[5]) + 1) + return _n + + + def find_focal_length(self): + _n = self.find_refractive_index(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + + def phase_delay(self, x, y, l, paraxial=False): + ''' + This function calculates the phase delay introduced by the lens. + + Input: + x, y - Coordinates of the lens + l - Wavelength in millimeters + paraxial - [bool] Apply a paraxial approximation? + ''' + _k = 2 * np.pi / l + _n = self.find_refractive_index(l) + + if not paraxial: + # For all non-paraxial lenses: + # Retrieve the sag of the lens surfaces for coordinates x,y + _A1 = self.set_first_surface_sag(x,y) + _A2 = self.set_second_surface_sag(x,y) + _dxy = self.thickness - _A1 + _A2 + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + elif paraxial: + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + if self.wavefrontError: + _t_l *= np.exp(-1j * _k * (_n - 1) * self.sag) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_l *= _lensMask + return _t_l + + + + + +class EvenAsphereLens(LensObject): + ''' + The EvenAsphereLens object describes an even aspherical lens and + finds the phase delay for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, thickness, R1, R2, k1, k2, a1, a2, CA, name=None): + super().__init__(CA, name) + assert glassCatalog.catalog.get(glass), 'Glass type not in catalog' + self.type = 'EVEN ASPHERE' + self.thickness = thickness # mm + self.R1 = R1 # mm + self.R2 = R2 # mm + self.conicConstantS1 = k1 + self.conicConstantS2 = k2 + self.asphereCoefficientS1 = a1 + self.asphereCoefficientS2 = a2 + self.glass = glass + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.f = self.find_focal_length() + + + @staticmethod + def calculate_surface_sag(x, y, R, k, a): + ''' + Defines the sag shape of the lens + Follows the ZEMAX definition on Even Aspheres + + Input parameters: + x, y - x,y coordinates of the lens in mm + R - Radius of curvature of the surface in mm + k - Conic constant of the surface + a - Coefficients of the polynomial expansion of the lens sag. + ''' + _r = np.sqrt(x**2 + y**2) + _c = 1 / R + _expansion = np.sum(np.asarray([a[i] * _r**( 2*(i+1) ) for i in range(len(a))])) + _z = _c * _r**2 / ( 1 + np.sqrt( 1 - (1 + k) * _c**2 * _r**2) ) + _expansion + return _z + + + def set_first_surface_sag(self, x,y): + ''' + Defines the shape of the first surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _first_surface_sag = self.calculate_surface_sag(x, y, self.R1, self.conicConstantS1, self.asphereCoefficientS1) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _first_surface_sag *= _lensMask + self.first_surface_sag = _first_surface_sag + return self.first_surface_sag + + + def set_second_surface_sag(self, x,y): + ''' + Defines the shape of the second surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _second_surface_sag = self.calculate_surface_sag(x, y, self.R2, self.conicConstantS2, self.asphereCoefficientS2) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _second_surface_sag *= _lensMask + self.second_surface_sag = _second_surface_sag + return self.second_surface_sag + + + def get_first_surface_sag(self): + return self.first_surface_sag + + + def get_second_surface_sag(self): + return self.second_surface_sag + + + def find_refractive_index(self, l): + # l in mm needs to be converted to l in micron + l *= 1e3 + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* l**2)/(l**2 - _a[1]) + (_a[2] * l**2) / (l**2 - _a[3]) + (_a[4] * l**2) / (l**2 - _a[5]) + 1) + return _n + + + def find_focal_length(self): + ''' + Calculates the focal length, where we assume that the + asphere component is small, so that the focal length is + approximately the same as for the spherical lens with + radii of curvature of R1 and R2. + ''' + _n = self.find_refractive_index(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + + def phase_delay(self, x, y, l, paraxial=False): + ''' + This function calculates the phase delay introduced by the lens. + + Input: + x, y - Coordinates of the lens + l - Wavelength in millimeters + paraxial - [bool] Apply a paraxial approximation? + ''' + _k = 2 * np.pi / l + _n = self.find_refractive_index(l) + + if not paraxial: + # For all non-paraxial lenses: + # Retrieve the sag of the lens surfaces for coordinates x,y + _A1 = self.set_first_surface_sag(x,y) + _A2 = self.set_second_surface_sag(x,y) + _dxy = self.thickness - _A1 + _A2 + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + elif paraxial: + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + if self.wavefrontError: + _t_l *= np.exp(-1j * _k * (_n - 1) * self.sag) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_l *= _lensMask + return _t_l + + + + + +class MirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + self.type = None + self.sag = None + self.wavefrontError = False + + + def add_wavefront_error(self, z): + assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue' + self.sag = z + self.wavefrontError = True + + + + + +class ParabolicMirror(MirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + MirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'PARABOLIC MIRROR' + + + def phase_delay(self, x, y, l, **kwargs): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + + if self.wavefrontError: + _sagPhase = self.sag + _t_m = np.exp(1j * _k * 2 * (_dz + _sagPhase) ) + else: + _t_m = np.exp(1j * _k * 2 * _dz) + + # Masking out the parts that fall outside the mirror clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _mirrorMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_m *= _mirrorMask + return _t_m + + + + + +class SphericalMirror(MirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + MirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.f = R/2 + self.CA = CA + self.type = 'SPHERICAL MIRROR' + + + def phase_delay(self, x, y, l, **kwargs): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + + if self.wavefrontError: + _sagPhase = self.sag + _t_m = np.exp(1j * _k * 2 * (_dz + _sagPhase) ) + else: + _t_m = np.exp(1j * _k * 2 * _dz) + + # Masking out the parts that fall outside the mirror clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _mirrorMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_m *= _mirrorMask + return _t_m + + + + + +class FreeformMirror(MirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + MirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'USER DEFINED MIRROR' + + + def phase_delay(self, l, **kwargs): + _k = 2 * np.pi / l + if self.wavefrontError: + _t_m = np.exp(1j * _k * 2 * ( self.z + self.sag) ) + else: + _t_m = np.exp(1j * _k * 2 * self.z) + return _t_m + + + + + +class OpticalSurface: + def __init__(self, mask=None, name=None): + self.type = 'Dummy surface' + if mask: + self.x, self.y, self.mask = mask + self.type = 'Mask' + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + + def phase_delay(self, x, y, l, **kwargs): + return np.ones(np.shape(x)) + + + + + +class ADCObject: + def __init__(self, wedge=(1.6554, 6.28927, -1.48306)): + self.p1 = Prism('S-LAH71', 15, wedge[0], wedge[1], CA=125, name='MCD-ADC-P1') + self.p2 = Prism('S-FPL51', 15, wedge[1], wedge[2], CA=125, name='MCD-ADC-P2') + self.p3 = Prism('S-FPL51', 15, wedge[2], wedge[1], CA=125, name='MCD-ADC-P3') + self.p4 = Prism('S-LAH71', 15, wedge[1], wedge[0], CA=125, name='MCD-ADC-P4') + self.theta = 90 + + + def set_orientation(self, theta): + self.p1.set_prism_orientation(theta) + self.p2.set_prism_orientation(theta) + self.p3.set_prism_orientation(180-1*theta) + self.p4.set_prism_orientation(180-1*theta) + self.theta = theta + + + def set_field_rotation(self, FR): + self.p1.set_prism_orientation(self.theta-FR) + self.p2.set_prism_orientation(self.theta-FR) + self.p3.set_prism_orientation(180-1*self.theta-FR) + self.p4.set_prism_orientation(180-1*self.theta-FR) + + + def get_phase_delay(self, x, y, l, theta=None, paraxial=False): + if theta is not None: + self.set_orientation(theta) + + _t_p1 = self.p1.phase_delay(x, y, l, theta=self.theta, paraxial=False) + _t_p2 = self.p2.phase_delay(x, y, l, theta=self.theta, paraxial=False) + _t_p3 = self.p3.phase_delay(x, y, l, theta=180-self.theta, paraxial=False) + _t_p4 = self.p4.phase_delay(x, y, l, theta=180-self.theta, paraxial=False) + return _t_p1, _t_p2, _t_p3, _t_p4 + + + def add_to_optical_train(self, opticalTrainObject, d, paraxial=True): + ''' + Adds the whole ADC object to an optical train object. + Because the ADC consists of multiple prisms, where each + prism has two surfaces, this is a bit more involved than + just doing opticalTrainObjec.add_object(ADC, dist=d) + + Input: + opticalTrainObject - optical train object, see class OpticalTrain + d - distance in mm to next object after the ADC + paraxial - [bool] if true, each ADC only acts as a phase + transformation. No diffraction will happen + inside the prism. + ''' + if not paraxial: + print('This feature has not been implemented yet. Please use paraxial mode.') + pass + + if paraxial: + opticalTrainObject.add_object(self.p1, dist=d) + opticalTrainObject.add_object(self.p2, dist=1) + opticalTrainObject.add_object(self.p3, dist=8) + opticalTrainObject.add_object(self.p4, dist=1) + + + + +class Prism: + def __init__(self, glass, thickness, tilt1, tilt2, CA, name=None): + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + self.type = 'PRISM' + self.glass = glass + self.thickness = thickness + self.tilt1x = tilt1 + self.tilt1y = 0 + self.tilt2x = tilt2 + self.tilt2y = 0 + self.CA = CA + self.sag_combined = None + self.sag1 = None + self.sag2 = None + self.wavefrontError = False + self.theta = 0 + + # Remember original tilts for reset: + self.tilt1x_OG = tilt1 + self.tilt1y_OG = 0 + self.tilt2x_OG = tilt2 + self.tilt2y_OG = 0 + + + def find_refractive_index(self, l): + # l in mm needs to be converted to l in micron + l *= 1e3 + if self.glass == 'S-FPL51': + a = [1.170105050E+00, 4.757107830E-02, 7.638324450E-01] + b = [6.162039240E-03, 2.633728760E-02, 1.418826420E+02] + elif self.glass == 'S-LAH71': + a = [1.982800310E+00, 3.167584500E-01, 2.444726460E+00] + b = [1.189874560E-02, 5.271560010E-02, 2.132206970E+02] + return self.calculate_dispersion(l, a, b) + + + def calculate_dispersion(self, l, a, b): + # l in mm needs to be converted to l in micron + l *= 1e3 + n = np.sqrt( (a[0]* l**2)/(l**2 - b[0]) + (a[1] * l**2) / (l**2 - b[1]) + (a[2] * l**2) / (l**2 - b[2]) + 1) + return n + + + def set_prism_orientation(self, theta): + self.tilt1x, self.tilt1y = self.tilt_rotation(self.tilt1x_OG, self.tilt1y_OG, theta) + self.tilt2x, self.tilt2y = self.tilt_rotation(self.tilt2x_OG, self.tilt2y_OG, theta) + self.theta = theta + + + def tilt_rotation(self, tx,ty, theta): + theta = np.deg2rad(theta-90) + tx_new = tx * np.cos(theta) - ty * np.sin(theta) + ty_new = tx * np.sin(theta) + ty * np.cos(theta) + return tx_new, ty_new + + + def get_surface_sag(self, x, y, theta): + self.set_prism_orientation(theta) + self.first_surface_sag = x * np.tan(np.deg2rad(self.tilt1x)) + y * np.tan(np.deg2rad(self.tilt1y)) + self.second_surface_sag = x * np.tan(np.deg2rad(self.tilt2x)) + y * np.tan(np.deg2rad(self.tilt2y)) + + # Masking out the parts that fall outside the prism clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _prismMask = (dist_from_center <= (self.CA/2)).astype(int) + self.first_surface_sag *= _prismMask + self.second_surface_sag *= _prismMask + return self.first_surface_sag, self.second_surface_sag + + + def add_wavefront_error(self, z, surface=0): + assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue' + assert surface in [0,1,2], 'Surface not understood. Use 0 for transmitted wavefront error, \ + 1 for surface 1 or 2 for surface 2.' + if surface == 0: + self.sag_combined = z + elif surface == 1: + self.sag1 = z + elif surface == 2: + self.sag2 = z + self.wavefrontError = True + + + def phase_delay(self, x, y, l, theta=None, paraxial=False): + ''' + This function calculates the phase delay introduced by the lens. + + Input: + x, y - Coordinates of the lens + l - Wavelength in millimeters + paraxial - [bool] Apply a paraxial approximation? In the + case of a prism, this means that the prism acts + only as a phase transformation. Diffraction + inside the prism will not take place. + ''' + if theta is None: + theta = self.theta + _k = 2 * np.pi / l + _n = self.find_refractive_index(l) + + # Retrieve the sag of the lens surfaces for coordinates x,y + _A1, _A2 = self.get_surface_sag(x,y, theta) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _prismMask = (dist_from_center <= (self.CA/2)).astype(int) + + if paraxial: + # Calculates the phase transformation of this prism. + # Diffraction inside the prism will not take place. + _dxy = self.thickness + _A1 + _A2 + _t_p = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + + if self.wavefrontError: + if self.sag_combined is not None: + _t_p *= np.exp(-1j * _k * (_n - 1) * self.sag_combined) + elif self.sag1 is not None: + _t_p *= np.exp(-1j * _k * (_n - 1) * self.sag1) + elif self.sag2 is not None: + _t_p *= np.exp(-1j * _k * (_n - 1) * self.sag2) + + # _t_p *= _prismMask + return _t_p + + if not paraxial: + # Return two different phase transformations + # Option to add surface form error (or wavefront error) + # to each interface. + _t_p_s1 = np.exp(-1j * _k * (_n - 1) * _A1) + _t_p_s2 = np.exp(-1j * _k * (_n - 1) * _A2) + + if self.wavefrontError: + if self.sag1 is not None: + _t_p_s1 *= np.exp(-1j * _k * (_n - 1) * self.sag1) + if self.sag2 is not None: + _t_p_s2 *= np.exp(-1j * _k * (_n - 1) * self.sag2) + if self.sag_combined is not None and self.sag1 is None and self.sag2 is None: + _t_p_s1 *= np.exp(-1j * _k * (_n - 1) * self.sag_combined) + + # _t_p_s1 *= _prismMask + # _t_p_s2 *= _prismMask + return _t_p_s1, _t_p_s2 + + + + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + + def print_lens_data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def fresnel_number(self, a, z, l): + return a**2 / (z * l) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _fou = FourierOptics(l=l) + + _wf = U_in + + oC = [x.min(), x.max(), y.min(), y.max()] + show = False + _wf, x, y = _fou.fresnel_propagation(_wf, x, y, dz=self.sequentialDistance[0], oversample=None, show=show, save=False, outputCoordinates=oC) + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + + for _lens in self.opticalTrain[:-1]: # Except for image plane + + title = f"{self.opticalTrain[:-1].index(_lens)+1:02d}" + print (' Calculating lens {0} of {1}'.format(title, f"{len(self.opticalTrain[:-1]):02d}"), end="\r") + + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i+1] + _wf *= _lens.phase_delay(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number(_lens.CA, _dz, l) + _F = 1 + + + if _F < 5: + # Fresnel propagation + if _lens == self.opticalTrain[-2]: + oC = [-2, 2, -2, 2] + show=True + _wf, x, y = _fou.fresnel_propagation(_wf, x, y, dz=self.sequentialDistance[_i+1], oversample=None, show=show, save=False, outputCoordinates=oC) + elif _F >= 5: + print('Using Plane Wave!') + # Angular spectrum (p2p) propagation + # _wf, x, y = self.plane_wave_propagation() + + pass + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + + + + +class WavefrontError: + ''' + This class contains methods to describe wavefront errors; + either by means of zernike polynomials (description and fits) + or by means of Power Spectral Density analysis. + ''' + def __init__(self, n, m): + self.n = n # Resolution in x-direction + self.m = m # Resolution in y-direction + self.resolution = (n,m) + + + def calculate_rnm(self,n,m,rho): + ''' + @author: Tibor Agocs + Return an array with the zernike Rnm polynomial calculated at rho points. + + + **ARGUMENTS:** + + === ========================================== + n n order of the Zernike polynomial + m m order of the Zernike polynomial + rho Matrix containing the radial coordinates. + === ========================================== + + .. note:: For rho>1 the returned value is 0 + + .. note:: Values for rho<0 are silently returned as rho=0 + + ''' + + if (n-m)%2!=0: + raise Exception('n-m must be even') + if abs(m)>n: + raise Exception('The following must be true |m|<=n') + + if(n==0 and m==0): + data=np.ones(rho.shape) + return data + _mask = rho > 1 + rho[_mask] = 0 + _Rnm = np.zeros(rho.shape) + _S = (n-np.abs(m))/2 + for s in range (0,int(_S+1)): + _CR =(-1)**s*factorial(n-s)/ \ + (factorial(s)*factorial(-s+(n+np.abs(m))/2)* \ + factorial(-s+(n-np.abs(m))/2)) + _p = _CR * ( rho**(n-2*s) ) + _Rnm = _Rnm + _p + _Rnm[_mask] = 0 + return _Rnm + + + def zernike_polynomial_polar(self,n,m,rho,theta): + ''' + @author: Tibor Agocs + Returns the an array with the Zernike polynomial evaluated in the rho and + theta. + + **ARGUMENTS:** + ===== ========================================== + n n order of the Zernike polynomial + m m order of the Zernike polynomial + rho Matrix containing the radial coordinates. + theta Matrix containing the angular coordinates + ===== ========================================== + + .. note:: For rho>1 the returned value is 0 + + .. note:: Values for rho<0 are silently returned as rho=0 + + **RETURNS** + ===== =========================================== + _Zmn Array with values of the Zernike polynomial + ===== =========================================== + ''' + + _Rnm = self.calculate_rnm(n,m,rho) + + # Normalization constant + _delta_m0 = bool(m==0) + _NC = np.sqrt(2*(n+1)/(1+_delta_m0)) + + if m >= 0: + _Zmn = _NC * _Rnm * np.cos(m*theta) + elif m < 0: + _Zmn = _NC * _Rnm * np.sin(m*theta) + return _Zmn + + + def zernike_polar(self, coefficient, rho, theta, noll=False): + ''' + Returns the full surface created from a list of coefficients, + using the Zernike Polynomials. + + rho must be between 0 and 1, if not it will be rescaled, + see also zernikePolynomialPolar() + + **ARGUMENTS:** + ============ ========================================== + coefficient n order of the Zernike polynomial + rho Matrix containing the radial coordinates. + theta Matrix containing the angular coordinates + noll Boolean to indicate to use noll indices + ============ ========================================== + + **RETURNS** + **RETURNS** + ===== =========================================== + _zP Array with values of the Zernike polynomial + ===== =========================================== + ''' + if np.max(rho) > np.sqrt(2): + print('Rescaling!') + rho = np.sqrt(2) * rho / np.max(rho) + + _termList = [] + for i in range(len(coefficient)): + _C = coefficient[i] + if noll: + _jTerm = self.convert_noll_to_standard(i+1) + _n,_m = self.i_to_nm(i) + _termList.append( _C * self.zernike_polynomial_polar(_n,_m,rho,theta) ) + _zP = sum(_termList) + return _zP + + + def zernike_cartesian(self, coefficient, x, y, noll=False, addToData=False): + ''' + Converts the cartesian grid x,y into polar + coordinates and returns the data, such that + it can be plotted with e.g. plt.imshow() + + **ARGUMENTS:** + + ============ ========================================== + coefficient n order of the Zernike polynomial + rho Matrix containing the radial coordinates. + theta Matrix containing the angular coordinates + noll Boolean to indicate to use noll indices + ============ ========================================== + + **RETURNS** + ===== =========================================== + _zP Array with values of the Zernike polynomial + ===== =========================================== + ''' + _x0 = (np.max(x) - np.min(x)) / 2 + np.min(x) + _y0 = (np.max(y) - np.min(y)) / 2 + np.min(y) + _rho = np.sqrt(( x - _x0 )**2 + ( y - _y0 )**2 ) + _theta = np.arctan2( ( y - _y0 ) , ( x - _x0 ) ) + _zP = self.zernike_polar(coefficient, _rho, _theta, noll) + if not addToData: + self.data = _zP + else: + self.data += _zP + self.rms = np.sqrt(np.mean(np.asarray(self.data)**2)) + self.ptv = np.abs(np.max(self.data)) + np.abs(np.min(self.data)) + return _zP + + + def i_to_nm(self, i): + ''' + Converts a single index to n and m for the Zernike Polynomials + ''' + n = ceil((-3+np.sqrt(9+8*i))/2) + m = 2 * i - n*(n+2) + return int(n),int(m) + + + def convert_noll_to_standard(self, j): + ''' + + ''' + conversionList = [1, 3, 2, 5, 4, 6, 9, 7, 8, 10, 15, 13, 11, 12, 14, 21, 19, 17, 16, 18, 20, 27, 25, 23, 22, 24, 26, 28, 35, 33, 31, 29, 30, 32, 34, 36, 45, 43, 41, 39, 37, 38, 40, 42, 44, 55, 53, 51, 49, 47, 46, 48, 50, 52, 54, 65, 63, 61, 59, 57, 56, 58, 60, 62, 64, 66, 77, 75, 73, 71, 69, 67, 68, 70, 72, 74, 76, 78, 91, 89, 87, 85, 83, 81, 79, 80, 82, 84, 86, 88, 90, 105, 103, 101, 99, 97, 95, 93, 92, 94, 96, 98, 100, 102, 104, 119, 117, 115, 113, 111, 109, 107, 106, 108, 110, 112, 114, 116, 118, 120] + i = conversionList.index(j) + return i + + + def fit_zernike_coefficients(self, data, x=None, y=None, modes=21): + ''' + Fits the zernike coefficients to a data set, using + that the zernike modes can be treated independently. + The modes are set following standard index notation. + We solve by an analytical least squares fit + Z * A = F + Z.T * Z * A = Z.T * F + A = (Z.T * Z)^-1 * Z.T * F + + **ARGUMENTS:** + + ===== ========================================== + data array; the data that needs fitting + x array; the x-coordinates of the data + y array; the y-coordinates of the data + modes integer; number of modes to be fitted + ===== ========================================== + + **RETURNS** + ===== =========================================== + _A Array with values of the Zernike polynomial + ===== =========================================== + ''' + if x is None and y is None: + y,x = np.mgrid[-1:1:self.n*1j, -1:1:self.m*1j] + _F = data.flatten()[:,np.newaxis] + _x = x.flatten() + _y = y.flatten() + _x0 = (np.max(_x) - np.min(_x)) / 2 + np.min(_x) + _y0 = (np.max(_y) - np.min(_y)) / 2 + np.min(_y) + _rho = np.sqrt( ( _x - _x0 )**2 + ( _y - _y0 )**2 ) + _theta = np.arctan2( ( _y - _y0 ), ( _x - _x0 ) ) + + _Zern = [] + for i in range(modes): + _n, _m = self.i_to_nm(i) + _Znm = self.zernike_polynomial_polar(_n,_m,_rho,_theta) + _Zern.append(_Znm) + _Znm = None + _Zern = np.asarray(_Zern).T + _Zern_transpose = _Zern.T + _pseudo_inv = np.linalg.pinv(np.dot(_Zern_transpose, _Zern)) + _int1 = np.dot(_pseudo_inv, _Zern_transpose) + _A = np.dot(_int1,_F) + self.zernikeCoefficients = _A[:,0] + return _A[:,0] + + + def scale_zernike_coefficients(self, c, rms): + # Scaling the coefficients to some total value + _rms_coeffic = np.sqrt(np.sum( np.asarray(c)**2)) + _scale_factor = rms / _rms_coeffic + _c = (_scale_factor * np.asarray(c)).tolist() + # _rms_coeffic = np.sqrt(np.sum( np.asarray(_c)**2)) + self.zernikeCoefficients = _c + return _c + + + def power_spectral_density(self, s0, p, rho_c, dx, dy, A): + # Lorentzian equation used as PSD function + m = self.m + n = self.n + UmList = [(M - ( m + 1) / 2) / (m * dx) for M in range(m)] + VnList = [(N - ( n + 1) / 2) / (n * dy) for N in range(n)] + VnList, UmList = np.meshgrid(UmList, VnList) + + _rho_mn_list = np.sqrt(UmList**2 + VnList**2) + _h0list = 1 / ( 1 + np.power( ( _rho_mn_list / rho_c ), p) ) + _h0_norm = np.sum(_h0list) + _rho_mn_list = np.asarray(_rho_mn_list).reshape(m,n) + + _PSD = (s0**2 * A) / (_h0_norm * (1 + np.power((_rho_mn_list/rho_c), p))) + self.PSD2D = _PSD + + return _PSD, _rho_mn_list + + + def phase_map(self, phase): + # Uniform distribution of the phase + _phase_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _phase_in[:] = phase + _phase_out = pyfftw.empty_aligned((self.n, self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _phase_in, _phase_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _phaseAngle = np.angle(_phase_out) + + return _phaseAngle + + + def generate_height_map(self, s0, p, rho_c, addToData=False): + ''' + This function generates a surface height distribution based + on a Lorentzian PSD profile. Following the paper by E. Sidick (2009). + + 2020-01-20: Removed "mean=1e-4" from function def. --> Did this do anything? + ''' + + # Calculation of the necessary constants + _dx = 1 / self.m + _dy = 1 / self.n + _A = (self.m * _dx) * (self.n * _dy) + _phase = np.random.uniform(0,1,(self.m,self.m)) + + # Calculating the PSD and phase + _PSD, _rho_mn_list = self.power_spectral_density(s0, p, rho_c, _dx, _dy, _A) + _phiMap = self.phase_map(_phase) + + # Shifting the coordinates for the FFT + _PSD = np.fft.ifftshift(_PSD) + + # Doing the fourier transform + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = np.exp( 1j * _phiMap) * np.sqrt(_A * np.abs(_PSD)) + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # The final height distribution + _hMap = 1 / ( _dx * _dy ) * _out + if not addToData: + self.data = _hMap.real + else: + self.data += _hMap.real + + self.rms = np.sqrt(np.mean(np.asarray(self.data)**2)) + self.ptv = np.abs(np.max(self.data)) + np.abs(np.min(self.data)) + return self.data + + + def scale_rms(self, factor, data=None, filteredDataFlag=False): + # Scale the surface data such that the RMS gets multiplied by factor + _factor = factor ** 2 + if data is None: + if filteredDataFlag: + self.scaledPSD2D = self.filteredPSD2D * _factor + _data = self.filteredData + else: + self.calculate_2D_PSD() + self.scaledPSD2D = self.PSD2D * _factor + _data = self.data + elif data is not None: + self.PSD2D = self.calculate_2D_PSD(data=data) + self.scaledPSD2D = self.PSD2D * _factor + _data = data + + # Fourier transform of the surface profile + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = _data + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _phase = np.angle(_out) + + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.scaledPSD2D)) + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + self.scaledData = _out.real + self.scaledrms = np.sqrt(np.mean(np.asarray(self.scaledData)**2)) + self.scaledptv = np.abs(np.max(self.scaledData)) + np.abs(np.min(self.scaledData)) + + return _out.real + + + def calculate_2D_PSD(self, data=None): + ''' Returns the 2D PSD map ''' + if data is None: + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = self.data + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + elif data is not None: + _n, _m = np.asarray(data).shape + _in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _in[:] = np.asarray(data) + _out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + if data is None: + self.D1 = np.fft.fftshift(_out) + self.PSD2D = np.abs(self.D1)**2 + return self.PSD2D + elif data is not None: + _D1 = np.fft.fftshift(_out) + _PSD2D = np.abs(_D1)**2 + return _PSD2D + + + def calculate_1D_PSD(self, data=None, filteredDataFlag=False, rmax=None): + ''' Returns the azimuthally averaged PSD map ''' + if data is None: + try: + if not filteredDataFlag: + self.PSD1D = self.azimuthal_average(rmax) + else: + self.filteredPSD1D = self.azimuthal_average(filteredDataFlag=filteredDataFlag) + return self.filteredPSD1D + except AttributeError: + self.calculate_2D_PSD(data) + self.PSD1D = self.azimuthal_average(rmax) + return self.PSD1D + elif data is not None: + _PSD2D = self.calculate_2D_PSD(data=data) + _PSD1D = self.azimuthal_average(PSD=_PSD2D) + return _PSD1D + + + def set_lowpass_filter(self, r, dpix=1): + ''' Returns a map where all frequencies higher than r have been filtered out ''' + self.calculate_2D_PSD() + # Masking out the parts that fall outside the lens clear aperture + _x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j] + _dist_from_center = np.sqrt(_x**2 + _y**2) * dpix + _mask = (_dist_from_center <= (r)).astype(int) + _maskedPSD2D = self.PSD2D.copy() + _maskedPSD2D *= _mask + _phase = np.angle(np.fft.fft2(self.data)) + self.filteredPSD2D = _maskedPSD2D + self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real + + + def set_highpass_filter(self, r, dpix=1): + '''Returns a map where all spatial frequencies lower than r have been filtered out''' + self.calculate_2D_PSD() + # Masking out the parts that fall outside the lens clear aperture + # x,y = np.ogrid[-self.resolution[0]/2:self.resolution[0]/2, -self.resolution[1]/2:self.resolution[1]/2] + _x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j] + _dist_from_center = np.sqrt(_x**2 + _y**2) * dpix + _mask = (_dist_from_center <= (r)).astype(int) + _maskedPSD2D = self.PSD2D.copy() + _maskedPSD2D *= (1-_mask) + _phase = np.angle(np.fft.fft2(self.data)) + self.filteredPSD2D = _maskedPSD2D + self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real + + + def set_bandpass_filter(self, rmin, rmax, dpix=1): + '''Returns a map where all spatial frequencies lower than r2 and higher than r1 have been filtered out''' + assert rmax > rmin, 'rmax < rmin, not physical!' + self.calculate_2D_PSD() + # Masking out the parts that fall outside the lens clear aperture + # x,y = np.ogrid[-self.resolution[0]/2:self.resolution[0]/2, -self.resolution[1]/2:self.resolution[1]/2] + _x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j] + _dist_from_center = np.sqrt(_x**2 + _y**2) * dpix + _mask_1 = (_dist_from_center <= (rmax)).astype(int) + _mask_2 = (_dist_from_center >= (rmin)).astype(int) + _maskedPSD2D = self.PSD2D.copy() + _maskedPSD2D *= (_mask_1 & _mask_2) + _phase = np.angle(np.fft.fft2(self.data)) + self.filteredPSD2D = _maskedPSD2D + self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real + + + def azimuthal_average(self, PSD=None, center=None, filteredDataFlag=False, rmax=None): + """ + Calculate the azimuthally averaged radial profile. + + image - The 2D image + center - The [x,y] pixel coordinates used as the center. The default is + None, which then uses the center of the image (including + fracitonal pixels). + + """ + + # Calculate the indices from the image + _y, _x = np.indices(self.resolution) + if not center: + center = np.array([(_x.max()-_x.min())/2.0, (_y.max()-_y.min())/2.0]) + + _r = np.hypot(_x - center[0], _y - center[1]) + + # Get sorted radii + _ind = np.argsort(_r.flat) + _r_sorted = _r.flat[_ind] + + if not filteredDataFlag and PSD is None: + _PSD = self.PSD2D + + elif PSD is None: + _PSD = self.filteredPSD2D + + elif PSD is not None: + _PSD = PSD + + if rmax == True: + _PSD[ _r > _x.max()/2 ] = np.nan + elif rmax: + _PSD[ _r > rmax ] = np.nan + + _i_sorted = _PSD.flat[_ind] + + # Get the integer part of the radii (bin size = 1) + _r_int = _r_sorted.astype(int) + + # Find all pixels that fall within each radial bin. + _deltar = _r_int[1:] - _r_int[:-1] # Assumes all radii represented + _rind = np.where(_deltar)[0] # location of changed radius + _nr = _rind[1:] - _rind[:-1] # number of radius bin + + # Cumulative sum to figure out sums for each radius bin + _csim = np.cumsum(_i_sorted, dtype=float) + _tbin = _csim[_rind[1:]] - _csim[_rind[:-1]] + _radial_prof = _tbin / _nr + + return _radial_prof + + + + +class PSFFitter: + ''' + Wrapper around scipy.optimize.curve_fit(), so that point spread functions can be fitted + using some common profiles (e.g. Gaussian, Moffat and Airy pattern profiles). + ''' + def __init__(self, x, y, psfdata): + self.x = x + self.y = y + self.psfdata = psfdata + + + def gaussian_PSF(self, xy, amplitude, x0, y0, sigma_x, sigma_y, theta, offset): + ''' + Elliptical 2D gaussian profile + + Input: + xy - Combined x,y coordinates. x and y should be a meshgrid + amplitude - amplitude of the profile, i.e. maximum of the profile + x0, y0 - shift in x and y of the central peak + sigma_x - width of the profile in x + sigma_y - width of the profile in y + theta - Rotation of the elliptical gaussian profile + offset - noise floor or sky background level + ''' + _x,_y = xy + _x0 = float(x0) + _y0 = float(y0) + _a = (np.cos(theta)**2)/(2*sigma_x**2) + (np.sin(theta)**2)/(2*sigma_y**2) + _b = -(np.sin(2*theta))/(4*sigma_x**2) + (np.sin(2*theta))/(4*sigma_y**2) + _c = (np.sin(theta)**2)/(2*sigma_x**2) + (np.cos(theta)**2)/(2*sigma_y**2) + _g = offset + amplitude*np.exp( - (_a*((_x-_x0)**2) + 2*_b*(_x-_x0)*(_y-_y0) + _c*((_y-_y0)**2))) + return _g.ravel() + + + def fit_gaussian_PSF(self, initialGuess=None): + ''' + Fit a gaussian PSF profile, a Moffat PSF profile and an Airy pattern + An OptimizeWarning is given when the best fit is reached, for some reason; + see also: + https://stackoverflow.com/questions/41725377/why-isnt-curve-fit-able-to-estimate-the-covariance-of-the-parameter-if-the-pa + + Input: + initialGuess - Tuple, containing (amplitude, x0, y0, sigma_x, sigma_y, theta, offset) + ''' + _popt, _pcov = opt.curve_fit(self.gaussian_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess) + return self.gaussian_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov + + + def moffat_PSF(self, xy, amplitude, x0, y0, fwhm1, fwhm2, theta, offset, beta): + ''' + Elliptical moffat distribution, often used to fit a PSF without diffraction rings + Equation taken from http://www.aspylib.com/doc/aspylib_fitting.html + As beta goes to infinity, the moffat profile approaches a gaussian; as mentioned + here https://www.gnu.org/software/gnuastro/manual/html_node/PSF.html + + Input: + xy - Combined x,y coordinates. x and y should be a meshgrid + amplitude - amplitude of the profile, i.e. maximum of the profile + x0, y0 - shift in x and y of the central peak + fwhm1, fwhm2 - width of the profile in perpendicular directions + theta - Rotation of the elliptical moffat profile + offset - noise floor or sky background level + beta - shape parameter of the moffat profile + ''' + _x,_y = xy + _x0 = float(x0) + _y0 = float(y0) + _alpha_1 = fwhm1 / (2 * np.sqrt(2**(1/beta) - 1)) + _alpha_2 = fwhm2 / (2 * np.sqrt(2**(1/beta) - 1)) + _a = (np.cos(theta)/_alpha_1)**2 + (np.sin(theta)/_alpha_2)**2 + _b = (np.sin(theta)/_alpha_1)**2 + (np.cos(theta)/_alpha_2)**2 + _c = np.sin(theta) * np.cos(theta) * (1/(_alpha_1**2) - 1/(_alpha_2**2)) + _m = offset + amplitude/((1 + _a*(_x-_x0)**2 + _b*(_y-_y0)**2 + 2*_c*(_x-_x0)*(_y-_y0))**beta) + return _m.ravel() + + + def fit_moffat_PSF(self, initialGuess=None): + ''' + Fit a Moffat PSF profile + As beta goes to infinity, the moffat profile approaches a gaussian; as mentioned + here https://www.gnu.org/software/gnuastro/manual/html_node/PSF.html + This might raise a RuntimeWarning. + + Input: + initialGuess - Tuple, containing (amplitude, x0, y0, fwhm1, fwhm2, theta, offset, beta) + ''' + _popt, _pcov = opt.curve_fit(self.moffat_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess) + return self.moffat_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov + + + def airy_PSF(self, xy, amplitude, x0, y0, offset, radius): + ''' + 2D Airy pattern with offset included + + Input: + xy - Combined x,y coordinates. x and y should be a meshgrid + amplitude - amplitude of the profile, i.e. maximum of the profile + x0, y0 - shift in x and y of the central peak + offset - noise floor or sky background level + ''' + _x,_y = xy + _x0 = float(x0) + _y0 = float(y0) + _r = np.sqrt((_x-_x0)**2 + (_y-_y0)**2) + _Rz = 1.2196698912665045 + _inp = (np.pi * _r) / (radius/_Rz) + _J1 = j1(_inp) + _fraction = np.nan_to_num(2*_J1 / _inp, nan=1.0) + _a = offset + amplitude * (_fraction)**2 + return _a.ravel() + + + def fit_airy_PSF(self, initialGuess=None): + ''' + Repackaging of scipy's curve_fit to fit an Airy pattern to the data. + No arguments are necessary, but an initial guess of the parameters + is recommended. + + Input: + initialGuess - Tuple, containing (amplitude, x0, y0, offset, radius) + ''' + _popt, _pcov = opt.curve_fit(self.airy_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess) + return self.airy_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov + + + def reference_PSF(self, p, xy, perturbedPSF): + ''' + Residual function used for the mimizer functions in + self.fit_custom_field(), see function below. + ''' + _x0, _y0 = p + _x, _y = xy + _a = self.f_Eref_real(_x - _x0, _y - _y0) + 1j * self.f_Eref_imag(_x - _x0, _y - _y0) + _coupling = self.get_coupling(_a, perturbedPSF) + return 1 - np.abs(_coupling)**2 + + + def fit_custom_field(self, Eref, Eper=None, method='local', niter_success=10, bounds=None, return_coupling=False): + ''' + Fit any custom PSF to the data (self.psfdata). It requires an input + of the custom PSF in the same shape as the data. Then a global + optimization algorithm (scipy.optimization.differential_evolution) + is performed to find the translation parameters; x0 and y0. + + Note: + The current implementation is not able to find other parameters, + such as amplitude, amplitude offset or radius. It assumes that + the custom PSF and data are quite similar in shape. + Also note that what is here called the PSF is actually the complex + field distribution of the PSF, i.e. not the intensity, but the + amplitude and phase information of the PSF. + + Input: + Eref - Custom PSF, including phase information + Eper - optional, perturbed PSF. By default self.psfdata is taken. + method - Optimization strategy to use. Choose between + 'local' + 'global' + 'local' first applies a local search and is very sensitive + to the initial guess. + 'global' searches a global minimum. This is less sensitive + to the initial guess, but requires much more computing + time. + niter_succes- From scipy.optimize.basinhopping: + Stop the run if the global minimum candidate remains the + same for this number of iterations. + bounds - Bounds for the local minimizer + + Returns: + _best_match - Best matched PSF amplitude and phase data. + _popt - Optimum parameters, i.e. translation of the centroid. + _pvar - Variances of the parameters. + _coupling - Optional; coupling factor + + ''' + + assert Eref.shape == self.psfdata.shape, 'The shape of the new (Eper) and reference PSF (Eref) do not match.' + # Set x and y coordinates for interpolation. No 2D meshgrid is necessary. + _x = self.x[0] + _y = self.y[:,0] + + # Check for custom psf data and custom bounds + if Eper is None: + Eper = self.psfdata + print('taken self.psfdata') + + # Create interpolation function to match amplitude and phase to x and y coordinates. + self.f_Eref_real = interpolate.interp2d( _x, _y, Eref.real, kind='cubic') + self.f_Eref_imag = interpolate.interp2d( _x, _y, Eref.imag, kind='cubic') + + # Determining a good initial guess + # Reasonable to assume >0.9 of the maximum amplitude is near the centroid position. + _max_amplitude = np.max(np.abs(Eref)) + _index_i, _index_j = np.where(np.abs(Eref) > (0.9 * _max_amplitude) ) + _x0_guess = np.mean(self.x[_index_i, _index_j]) + _y0_guess = np.mean(self.y[_index_i, _index_j]) + _p0 = [_x0_guess, _y0_guess] + + if method == 'global': + # Arguments for basinhopping function (i.e. global search function) + _min_kwargs = {"args":((_x,_y), Eper), "jac":False} + _popt = opt.basinhopping(self.reference_PSF, _p0, minimizer_kwargs=_min_kwargs, niter_success=niter_success) + _p0 = _popt.x # Set optimal parameters as new first guess. + method = 'local' # Switch to local minimzer to also calculate the uncertainties + + if method == 'local': + # Mimizer function using L-BFGS-B method. + if bounds is None: + bounds = [(_x.min(), _x.max()),(_y.min(), _y.max())] + _res = opt.minimize(self.reference_PSF, _p0, bounds=bounds, args=((_x,_y), Eper) ) + _popt = _res.x + + # Getting the uncertainty in the minimum + _ftol = 2.220446049250313e-09 # ftol assuming L-BFGS-B method in the minimizer. + _hess_inv_i = np.diag(_res.hess_inv.todense()) + _pvar = max(1, abs(_res.fun)) * _ftol * _hess_inv_i + + # Centroid coordinates + _x0_opt, _y0_opt = _popt + _best_match = self.f_Eref_real(_x - _x0_opt, _y - _y0_opt) + 1j * self.f_Eref_imag(_x - _x0_opt, _y - _y0_opt) + + if return_coupling: + _coupling = self.get_coupling(_best_match, Eper) + return _best_match, _popt, _pvar, _coupling + else: + return _best_match, _popt, _pvar + + + def get_coupling(self, Eref, Eper): + ''' + Calculates the coupling integral as given by eq. 6.5 in + Quasi-optical systems by Goldsmith. + + This complex value is a measure of the match between a reference + field and a perturbed field. If the complex coupling factor is c, + then |c|**2 is the degradation of the perturbed field relative to + the reference field, not unlike the Strehl ratio. + + Parameters: + Eref - Array; Reference field + Eper - Array; Perturbed field + + Returns: + _c - Complex float; Coupling factor + ''' + _c = np.sum(Eref * np.conj(Eper))/ np.sqrt(np.sum(np.abs(Eref)**2)*np.sum(np.abs(Eper)**2)) + return _c + + + + + + + + + + + + diff --git a/fourierOptics/gaussian.py b/fourierOptics/gaussian.py new file mode 100644 index 0000000..9e79cda --- /dev/null +++ b/fourierOptics/gaussian.py @@ -0,0 +1,113 @@ +import numpy as np + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + + def fundamental_mode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beam_radius(z) + _R = self.curvature(z) + _phi0 = self.phase_shift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + + def curvature(self, z): + return z + self.z_c **2 / z + + + def beam_radius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + + def phase_shift(self, z): + return np.arctan(z/self.z_c) + + + def relative_power(self, r, z): + return np.exp(-2 * (r / self.beam_radius(z))**2) + + + + + +class GaussianBeamPropagation(GaussianBeam): + def __init__(self, gaussianBeamObject): + self.inputBeam = gaussianBeamObject + self.l = self.inputBeam.l + self.z_c = self.inputBeam.z_c + self.theta_0 = self.inputBeam.theta_0 + self.w0 = self.inputBeam.w0 + self.fwhm = self.inputBeam.fwhm + self.beam_width = self.w0 + + + def propagate_with_a_lens(self, ABCD, d_out, n, m, d_in=0, rmax=None): + ''' + Propagates the gaussian beam through a lens with the ray transfer matrix ABCD. + This follows the equations of Ch. 3.3 of Goldsmith's Quasi-optical Systems + + Parameters: + ABCD - Ray transfer matrix, use ABCD_paraxial_lens to calculate + this matrix for a paraxial lens, with focal length f. + d_out - Distance after the lens over which the beam is propagated. + d_in - Distance from the beam waist (w0) to the lens. Default is 0. + rmax - Maximum radius for which to calculate the beam profile. + If None, it will be set to three times the beam width at + the output distance. + + Output: + The beam information. Can be plotted using imshow() + The beam width at the given point can be retrieved by using self.beam_width + + ''' + _A,_B,_C,_D = ABCD + _focalDistance = - ( (_A * d_in + _B) * (_C * d_in + _D) + _A * _C * self.z_c**2 ) / ( (_C * d_in + _D)**2 + _C**2 * self.z_c**2 ) + _z = d_out - _focalDistance + + if d_out > _focalDistance: + d_l = d_out - _focalDistance + d_out = _focalDistance + _w0_out = self.w0 / np.sqrt((_C * d_in + _D)**2 + _C**2 * self.z_c**2) + _z_c_new = np.pi * _w0_out**2 / self.l + _wl_out = _w0_out * np.sqrt(1 + (d_l/_z_c_new)**2) + + # _q_out = ((_A + _C * d_out )*1j*self.z_c + (_A + _C * d_out)*d_in + (_B + _D*d_out)) / (_C * 1j * self.z_c + _C * d_in + _D) + _w0_out = self.w0 / np.sqrt((_C * d_in + _D)**2 + _C**2 * self.z_c**2) + + self.z_c = np.pi * _w0_out**2 / self.l + self.w0 = _w0_out + self.theta_0 = self.l / (np.pi * _w0_out) + self.fwhm = 1.18 * self.theta_0 + self.beam_width = self.beam_radius(_z) + + if rmax is None: + rmax = 3 * self.beam_width + + _y, _x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + _r = np.sqrt(_x**2 + _y**2) + if _z != 0: + _w = self.beam_radius(_z) + _R = self.curvature(_z) + _phi0 = self.phase_shift(_z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + + def ABCD_paraxial_lens(self, f): + return [1, 0, -1/f, 1] \ No newline at end of file diff --git a/fourierOptics/misc.py b/fourierOptics/misc.py new file mode 100644 index 0000000..7c90be2 --- /dev/null +++ b/fourierOptics/misc.py @@ -0,0 +1,95 @@ +import numpy as np +import pickle + +''' + Miscellaneous functions that do not belong specifically to any of the + other files. + + Author: Joost van den Born + Contact: born@astron.nl +''' + + +def perform_zero_padding(A, xs, ys): + ''' + Add zero padding around the array + + Input: + A - 2D-Input array + xs,ys - The new shape after padding. + + Output: + - Array with the new padded zeros + + ''' + assert A.shape[0] <= xs and A.shape[1] <= ys, 'New size is smaller than array size.' + x_margin = int((xs - A.shape[0])/2) + y_margin = int((ys - A.shape[1])/2) + return np.pad(A, (x_margin, y_margin)) + +def find_nearest(array, value): + ''' + For a value it finds the nearest element in an array + Returns the index of that element in the original + array. + ''' + array = np.asarray(array) + diff = (np.abs(array - value)).argmin() + idx = np.unravel_index(diff, array.shape) + return idx + +def find_maximum(array): + ''' + Finds the index of the maximum of the given array + ''' + idx = np.unravel_index(array.argmax(), array.shape) + return idx + +def select_subsection(x, y , outputCoordinates): + ''' + Returns the indices of a subsection of an x,y meshgrid + ''' + select_1 = find_nearest(x, outputCoordinates[0])[1] + select_2 = find_nearest(x, outputCoordinates[1])[1] + select_3 = find_nearest(y, outputCoordinates[2])[0] + select_4 = find_nearest(y, outputCoordinates[3])[0] + return [select_1, select_2, select_3, select_4] + +def perform_extrapolation(x, y, n, m): + ''' + Performs linear extrapolation of a meshgrid to a new + shape n,m + + Returns the new extrapolated x,y meshgrid + ''' + + # Determine properties of x,y meshgrid + xshape = x.shape[0] + yshape = y.shape[0] + dpix = (np.abs(np.min(x)) + np.abs(np.max(x))) / (xshape-1) + + # Extrapolate from, and include, the old x,y meshgrid + xnew = np.linspace(-(n-1)/2*dpix, (n-1)/2*dpix, n) + ynew = np.linspace(-(m-1)/2*dpix, (m-1)/2*dpix, m) + xnew, ynew = np.meshgrid(xnew, ynew) + return xnew, ynew + + +def save_variable(filename, variable): + ''' + Dumps the given variable to a binary pickle file + for later use. + ''' + with open(filename, 'wb') as f: + pickle.dump(variable, f, protocol=-1 ) + + +def load_variable(filename): + ''' + Loads a binary pickle file for use. + ''' + with open(filename, 'rb') as f: + variable = pickle.load(f) + return variable + + diff --git a/fourierOptics/opticalTrain.py b/fourierOptics/opticalTrain.py new file mode 100644 index 0000000..3f123df --- /dev/null +++ b/fourierOptics/opticalTrain.py @@ -0,0 +1,103 @@ +import numpy as np +from tabulate import tabulate + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + + def print_lens_data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def fresnel_number(self, a, z, l): + return a**2 / (z * l) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _fou = FourierOptics(l=l) + + _wf = U_in + + oC = [x.min(), x.max(), y.min(), y.max()] + show = False + _wf, x, y = _fou.fresnel_propagation(_wf, x, y, dz=self.sequentialDistance[0], oversample=None, show=show, save=False, outputCoordinates=oC) + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + + for _lens in self.opticalTrain[:-1]: # Except for image plane + + title = f"{self.opticalTrain[:-1].index(_lens)+1:02d}" + print (' Calculating lens {0} of {1}'.format(title, f"{len(self.opticalTrain[:-1]):02d}"), end="\r") + + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i+1] + _wf *= _lens.phase_delay(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number(_lens.CA, _dz, l) + _F = 1 + + + if _F < 5: + # Fresnel propagation + if _lens == self.opticalTrain[-2]: + oC = [-2, 2, -2, 2] + show=True + _wf, x, y = _fou.fresnel_propagation(_wf, x, y, dz=self.sequentialDistance[_i+1], oversample=None, show=show, save=False, outputCoordinates=oC) + elif _F >= 5: + print('Using Plane Wave!') + # Angular spectrum (p2p) propagation + # _wf, x, y = self.plane_wave_propagation() + + pass + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) diff --git a/fourierOptics/optics.py b/fourierOptics/optics.py new file mode 100644 index 0000000..e1c0f48 --- /dev/null +++ b/fourierOptics/optics.py @@ -0,0 +1,773 @@ +import numpy as np +from scipy.optimize import fsolve +from .data import glassCatalog + +class LensObject: + def __init__(self, CA, name=None): + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + self.CA = CA + self.type = None + self.f = None + self.sag = None + self.wavefrontError = False + + def add_wavefront_error(self, z): + assert len(z.shape)==2, 'Wavefront data is not 2D. Cannot continue' + self.sag = z + self.wavefrontError = True + + + + + +class ParaxialLens(LensObject): + def __init__(self, f, CA, name=None): + super().__init__(CA, name) + self.f = f + self.type = 'PARAXIAL' + + + def find_focal_length(self): + return self.f + + + def phase_delay(self, x, y, l, **kwargs): + _k = 2 * np.pi / l + _t_l = np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + if self.wavefrontError: + _t_l *= np.exp(-1j * _k * self.sag / l) + + # Masking out the parts that fall outside the lens clear aperture + # dist_from_center = np.sqrt(x**2 + y**2) + # _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + # _t_l *= _lensMask + return _t_l + + + + + +class SphericalLens(LensObject): + ''' + The SphericalLens object describes any spherical lens and + finds the phase delay for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, thickness, R1, R2, CA, name=None): + super().__init__(CA, name) + assert glassCatalog.catalog.get(glass), 'Glass type not in catalog' + self.type = 'SPHERICAL' + self.thickness = thickness # mm + self.R1 = R1 # mm + self.R2 = R2 # mm + self.glass = glass + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35e-3 # mm + self.f = self.find_focal_length() + + + def set_first_surface_sag(self, x,y): + ''' + Defines the shape of the first surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _first_surface_sag = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _first_surface_sag *= _lensMask + self.first_surface_sag = _first_surface_sag + return self.first_surface_sag + + + def set_second_surface_sag(self, x,y): + ''' + Defines the shape of the second surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _second_surface_sag = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _second_surface_sag *= _lensMask + self.second_surface_sag = _second_surface_sag + return self.second_surface_sag + + + def get_first_surface_sag(self): + return self.first_surface_sag + + + def get_second_surface_sag(self): + return self.second_surface_sag + + + def find_refractive_index(self, l): + # l in mm needs to be converted to l in micron + l *= 1e3 + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* l**2)/(l**2 - _a[1]) + (_a[2] * l**2) / (l**2 - _a[3]) + (_a[4] * l**2) / (l**2 - _a[5]) + 1) + return _n + + + def find_focal_length(self): + _n = self.find_refractive_index(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + + def phase_delay(self, x, y, l, paraxial=False): + ''' + This function calculates the phase delay introduced by the lens. + + Input: + x, y - Coordinates of the lens + l - Wavelength in millimeters + paraxial - [bool] Apply a paraxial approximation? + ''' + _k = 2 * np.pi / l + _n = self.find_refractive_index(l) + + if not paraxial: + # For all non-paraxial lenses: + # Retrieve the sag of the lens surfaces for coordinates x,y + _A1 = self.set_first_surface_sag(x,y) + _A2 = self.set_second_surface_sag(x,y) + _dxy = self.thickness - _A1 + _A2 + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + elif paraxial: + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + if self.wavefrontError: + _t_l *= np.exp(-1j * _k * (_n - 1) * self.sag) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_l *= _lensMask + return _t_l + + + + + +class EvenAsphereLens(LensObject): + ''' + The EvenAsphereLens object describes an even aspherical lens and + finds the phase delay for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, thickness, R1, R2, k1, k2, a1, a2, CA, name=None): + super().__init__(CA, name) + assert glassCatalog.catalog.get(glass), 'Glass type not in catalog' + self.type = 'EVEN ASPHERE' + self.thickness = thickness # mm + self.R1 = R1 # mm + self.R2 = R2 # mm + self.conicConstantS1 = k1 + self.conicConstantS2 = k2 + self.asphereCoefficientS1 = a1 + self.asphereCoefficientS2 = a2 + self.glass = glass + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.f = self.find_focal_length() + + + @staticmethod + def calculate_surface_sag(x, y, R, k, a): + ''' + Defines the sag shape of the lens + Follows the ZEMAX definition on Even Aspheres + + Input parameters: + x, y - x,y coordinates of the lens in mm + R - Radius of curvature of the surface in mm + k - Conic constant of the surface + a - Coefficients of the polynomial expansion of the lens sag. + ''' + _r = np.sqrt(x**2 + y**2) + _c = 1 / R + _expansion = np.sum(np.asarray([a[i] * _r**( 2*(i+1) ) for i in range(len(a))])) + _z = _c * _r**2 / ( 1 + np.sqrt( 1 - (1 + k) * _c**2 * _r**2) ) + _expansion + return _z + + + def set_first_surface_sag(self, x,y): + ''' + Defines the shape of the first surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _first_surface_sag = self.calculate_surface_sag(x, y, self.R1, self.conicConstantS1, self.asphereCoefficientS1) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _first_surface_sag *= _lensMask + self.first_surface_sag = _first_surface_sag + return self.first_surface_sag + + + def set_second_surface_sag(self, x,y): + ''' + Defines the shape of the second surface and bound the corresponding sag function to + the lens object. + + Input: + x,y - Coordinates of the sag. + ''' + # A spherical lens surface requires only the radius of curvature + _second_surface_sag = self.calculate_surface_sag(x, y, self.R2, self.conicConstantS2, self.asphereCoefficientS2) + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _second_surface_sag *= _lensMask + self.second_surface_sag = _second_surface_sag + return self.second_surface_sag + + + def get_first_surface_sag(self): + return self.first_surface_sag + + + def get_second_surface_sag(self): + return self.second_surface_sag + + + def find_refractive_index(self, l): + # l in mm needs to be converted to l in micron + l *= 1e3 + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* l**2)/(l**2 - _a[1]) + (_a[2] * l**2) / (l**2 - _a[3]) + (_a[4] * l**2) / (l**2 - _a[5]) + 1) + return _n + + + def find_focal_length(self): + ''' + Calculates the focal length, where we assume that the + asphere component is small, so that the focal length is + approximately the same as for the spherical lens with + radii of curvature of R1 and R2. + ''' + _n = self.find_refractive_index(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + + def phase_delay(self, x, y, l, paraxial=False): + ''' + This function calculates the phase delay introduced by the lens. + + Input: + x, y - Coordinates of the lens + l - Wavelength in millimeters + paraxial - [bool] Apply a paraxial approximation? + ''' + _k = 2 * np.pi / l + _n = self.find_refractive_index(l) + + if not paraxial: + # For all non-paraxial lenses: + # Retrieve the sag of the lens surfaces for coordinates x,y + _A1 = self.set_first_surface_sag(x,y) + _A2 = self.set_second_surface_sag(x,y) + _dxy = self.thickness - _A1 + _A2 + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + elif paraxial: + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + if self.wavefrontError: + _t_l *= np.exp(-1j * _k * (_n - 1) * self.sag) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_l *= _lensMask + return _t_l + + + + + +class MirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + self.type = None + self.sag = None + self.wavefrontError = False + + + def add_wavefront_error(self, z): + assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue' + self.sag = z + self.wavefrontError = True + + + + + +class ParabolicMirror(MirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + MirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'PARABOLIC MIRROR' + + + def phase_delay(self, x, y, l, **kwargs): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + + if self.wavefrontError: + _sagPhase = self.sag + _t_m = np.exp(1j * _k * 2 * (_dz + _sagPhase) ) + else: + _t_m = np.exp(1j * _k * 2 * _dz) + + # Masking out the parts that fall outside the mirror clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _mirrorMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_m *= _mirrorMask + return _t_m + + + + + +class SphericalMirror(MirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + MirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.f = R/2 + self.CA = CA + self.type = 'SPHERICAL MIRROR' + + + def phase_delay(self, x, y, l, **kwargs): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + + if self.wavefrontError: + _sagPhase = self.sag + _t_m = np.exp(1j * _k * 2 * (_dz + _sagPhase) ) + else: + _t_m = np.exp(1j * _k * 2 * _dz) + + # Masking out the parts that fall outside the mirror clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _mirrorMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_m *= _mirrorMask + return _t_m + + + + + +class FreeformMirror(MirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + MirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'USER DEFINED MIRROR' + + + def phase_delay(self, l, **kwargs): + _k = 2 * np.pi / l + if self.wavefrontError: + _t_m = np.exp(1j * _k * 2 * ( self.z + self.sag) ) + else: + _t_m = np.exp(1j * _k * 2 * self.z) + return _t_m + + + + + +class OpticalSurface: + def __init__(self, mask=None, name=None): + self.type = 'Dummy surface' + if mask: + self.x, self.y, self.mask = mask + self.type = 'Mask' + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + + def phase_delay(self, x, y, l, **kwargs): + return np.ones(np.shape(x)) + + + + + +class ADCObject: + def __init__(self, wedge=(1.6554, 6.28927, -1.48306)): + self.p1 = Prism('S-LAH71', 15, wedge[0], wedge[1], CA=125, name='MCD-ADC-P1') + self.p2 = Prism('S-FPL51', 15, wedge[1], wedge[2], CA=125, name='MCD-ADC-P2') + self.p3 = Prism('S-FPL51', 15, wedge[2], wedge[1], CA=125, name='MCD-ADC-P3') + self.p4 = Prism('S-LAH71', 15, wedge[1], wedge[0], CA=125, name='MCD-ADC-P4') + self.theta = None + self.set_orientation(90) + + + def set_orientation(self, theta): + self.p1.set_prism_orientation(theta) + self.p2.set_prism_orientation(theta) + self.p3.set_prism_orientation(180-1*theta) + self.p4.set_prism_orientation(180-1*theta) + self.theta = theta + + + def set_field_rotation(self, FR): + self.p1.set_prism_orientation(self.theta-FR) + self.p2.set_prism_orientation(self.theta-FR) + self.p3.set_prism_orientation(180-1*self.theta-FR) + self.p4.set_prism_orientation(180-1*self.theta-FR) + + + def get_phase_delay(self, x, y, l, theta=None, paraxial=False, prism=None): + if theta is not None: + self.set_orientation(theta) + else: + theta = self.theta + if not prism: + _t_p1 = self.p1.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial) + _t_p2 = self.p2.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial) + _t_p3 = self.p3.phase_delay(x, y, l)#, theta=180-theta, paraxial=paraxial) + _t_p4 = self.p4.phase_delay(x, y, l)#, theta=180-theta, paraxial=paraxial) + return _t_p1, _t_p2, _t_p3, _t_p4 + elif prism == 1: + _t_p1 = self.p1.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial) + return _t_p1 + elif prism == 2: + _t_p2 = self.p2.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial) + return _t_p2 + elif prism == 3: + _t_p3 = self.p3.phase_delay(x, y, l)#, theta=180-1*theta, paraxial=paraxial) + return _t_p3 + elif prism == 4: + _t_p4 = self.p4.phase_delay(x, y, l)#, theta=180-1*theta, paraxial=paraxial) + return _t_p4 + else: + print('This prism ({}) does not exist.'.format(prism)) + + + + def add_to_optical_train(self, opticalTrainObject, d, paraxial=True): + ''' + Adds the whole ADC object to an optical train object. + Because the ADC consists of multiple prisms, where each + prism has two surfaces, this is a bit more involved than + just doing opticalTrainObjec.add_object(ADC, dist=d) + + Input: + opticalTrainObject - optical train object, see class OpticalTrain + d - distance in mm to next object after the ADC + paraxial - [bool] if true, each ADC only acts as a phase + transformation. No diffraction will happen + inside the prism. + ''' + if not paraxial: + print('This feature has not been implemented yet. Please use paraxial mode.') + pass + + if paraxial: + opticalTrainObject.add_object(self.p1, dist=d) + opticalTrainObject.add_object(self.p2, dist=1) + opticalTrainObject.add_object(self.p3, dist=8) + opticalTrainObject.add_object(self.p4, dist=1) + + def calculate_dispersion(self, l1, l2, hx=0, hy=0, unit='millimeter'): + ''' + Calculates the dispersion for the ADC (using geometric optics analysis) + at its current position, for a normalized on sky field angle (hx, hy). + Default field position is in the center of the FoV. + + Input: + l1, l2 - Wavelength range of interest (in millimeter) + hx, hy - Field of view of the instrument (normalized between -1 and 1) + + Output: + dispX, dispY - The dispersion in sky angles in milli arc seconds. + + This particular ADC model is only valid for an ADC located in the pupil of the instrument. + Change the pupil ratio (Diameter_instrument_pupil / Diameter_Entrance_Pupil) accordingly + + ''' + if unit == 'millimeter': + l1 *= 1e3 + l2 *= 1e3 + elif unit == 'micron': + pass + + _prismList = [self.p1, self.p2, self.p3, self.p4] + _d1x_list, _d1y_list = [], [] + # _pupilRatio = 0.07943/38.542 + _pupilRatio = 0.0801/38.542 + + hx = hx * 7.5e-3 + hy = hy * 7.5e-3 + ix = hx * 1/_pupilRatio + iy = hy * 1/_pupilRatio + + for l in [l1,l2]: + _deltaListX = np.zeros(len(_prismList)) + _deltaListY = np.zeros(len(_prismList)) + for p in range(4): + _n_g = _prismList[p].find_refractive_index(l, unit='micron') + + _b1x = _prismList[p].tilt1x + _b1y = _prismList[p].tilt1y + + _b2x = _prismList[p].tilt2x + _b2y = _prismList[p].tilt2y + + if p==0: + _t_iz_n_x = ix # Zero before first prism + _t_iz_n_y = iy # Zero before first prism + + # X direction + _t_i1_n_x = _t_iz_n_x #+ _b1x + _d1x = self.calculate_deviation(_t_i1_n_x, _b1x, _b2x, _n_g) + _t_tz_n_x = _t_iz_n_x - _d1x + _t_iz_n_x = _t_tz_n_x # Set output to new input (for the next prism) + + # Y direction + _t_i1_n_y = _t_iz_n_y #+ _b1y + _d1y = self.calculate_deviation(_t_i1_n_y, _b1y, _b2y, _n_g) + _t_tz_n_y = _t_iz_n_y - _d1y + _t_iz_n_y = _t_tz_n_y # Set output to new input (for the next prism) + + # Adding the deviations to the list with delta terms + _deltaListX[p] = _d1x + _deltaListY[p] = _d1y + _d1x_list.append(_deltaListX.sum()) + _d1y_list.append(_deltaListY.sum()) + + self.dispX = ( _d1x_list[1] - _d1x_list[0] ) * 3600 * 1000 * _pupilRatio + self.dispY = ( _d1y_list[1] - _d1y_list[0] ) * 3600 * 1000 * _pupilRatio + return self.dispX, self.dispY + + + @staticmethod + def calculate_deviation(t_i1, b1, b2, n): + ''' + Returns deviation of the ray after the prism with apex angle alpha + only now using Snell's law only. Using the formula from Hagen & Tkazcyk (2011) + Units: degrees + ''' + t_i1 = np.deg2rad(t_i1) + b1 = np.deg2rad(b1) + b2 = np.deg2rad(b2) + alpha = b2 - b1 + delta = t_i1 - b2 - np.arcsin( n * np.sin( np.arcsin(1/n * np.sin(t_i1 - b1) ) - alpha ) ) + return np.degrees(delta) + + def find_optimum_angle(self, inputDispersion, l1, l2, x0=50): + ''' + Simple minimizer to find the (local) minimum for a best position of the ADC. + ''' + _solver = fsolve(self.optimizer, x0=x0, args=(self, inputDispersion, l1, l2)) + return _solver[0] + + @staticmethod + def optimizer(theta, adcObject, inputDispersion, l1, l2, FR=None): + ''' + Minimization function to find the optimum rotation angle + Input: + theta - Angle in degrees relative to initial position + adcObject - the ADC class + inputDispersion - The amount of dispersion to correct for (in mas) + l1, l2 - The wavelength range to correct for (micrometer) + FR - Field rotation, not used currently. + ''' + if FR: + FRdeg = np.deg2rad(FR) + inputDispersionX = np.cos(FRdeg) * inputDispersion + inputDispersionY = np.sin(FRdeg) * inputDispersion + adcObject.set_orientation(float(theta)) + ADCdisp = adcObject.calculate_dispersion(l1,l2) + return np.sqrt((ADCdisp[1] - inputDispersionY)**2 + (ADCdisp[0] - inputDispersionX)**2) + else: + adcObject.set_orientation(float(theta)) + ADCdisp = adcObject.calculate_dispersion(l1,l2) + ADCdisp = ADCdisp[1] + return ADCdisp - inputDispersion + + + +class Prism: + def __init__(self, glass, thickness, tilt1, tilt2, CA, name=None): + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + self.type = 'PRISM' + self.glass = glass + self.thickness = thickness + self.tilt1x = tilt1 + self.tilt1y = 0 + self.tilt2x = tilt2 + self.tilt2y = 0 + self.CA = CA + self.sag_combined = None + self.sag1 = None + self.sag2 = None + self.wavefrontError = False + self.thetaPrism = 0 + + # Remember original tilts for reset: + self.tilt1x_OG = tilt1 + self.tilt1y_OG = 0 + self.tilt2x_OG = tilt2 + self.tilt2y_OG = 0 + + + def find_refractive_index(self, l, unit='millimeter'): + if unit == 'millimeter': + l *= 1e3 + elif unit == 'micron': + pass + if self.glass == 'S-FPL51': + a = [1.170105050E+00, 4.757107830E-02, 7.638324450E-01] + b = [6.162039240E-03, 2.633728760E-02, 1.418826420E+02] + elif self.glass == 'S-LAH71': + a = [1.982800310E+00, 3.167584500E-01, 2.444726460E+00] + b = [1.189874560E-02, 5.271560010E-02, 2.132206970E+02] + return self.calculate_refractive_index(l, a, b, unit='micron') + + + def calculate_refractive_index(self, l, a, b, unit='millimeter'): + # l in mm needs to be converted to l in micron + if unit == 'millimeter': + l *= 1e3 + elif unit == 'micron': + pass + n = np.sqrt( (a[0]* l**2)/(l**2 - b[0]) + (a[1] * l**2) / (l**2 - b[1]) + (a[2] * l**2) / (l**2 - b[2]) + 1) + return n + + + def set_prism_orientation(self, theta): + self.tilt1x, self.tilt1y = self.tilt_rotation(self.tilt1x_OG, self.tilt1y_OG, theta) + self.tilt2x, self.tilt2y = self.tilt_rotation(self.tilt2x_OG, self.tilt2y_OG, theta) + self.thetaPrism = theta + + + def tilt_rotation(self, tx,ty, theta): + theta = np.deg2rad(theta-90) + tx_new = tx * np.cos(theta) - ty * np.sin(theta) + ty_new = tx * np.sin(theta) + ty * np.cos(theta) + return tx_new, ty_new + + + def get_surface_sag(self, x, y, theta): + # self.set_prism_orientation(theta) + self.first_surface_sag = y * np.tan(np.deg2rad(self.tilt1x)) + x * np.tan(np.deg2rad(self.tilt1y)) + self.second_surface_sag = y * np.tan(np.deg2rad(self.tilt2x)) + x * np.tan(np.deg2rad(self.tilt2y)) + + # Masking out the parts that fall outside the prism clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _prismMask = (dist_from_center <= (self.CA/2)).astype(int) + self.first_surface_sag *= _prismMask + self.second_surface_sag *= _prismMask + return self.first_surface_sag, -1*self.second_surface_sag + + + def add_wavefront_error(self, z, surface=0): + assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue' + assert surface in [0,1,2], 'Surface not understood. Use 0 for transmitted wavefront error, \ + 1 for surface 1 or 2 for surface 2.' + if surface == 0: + self.sag_combined = z + elif surface == 1: + self.sag1 = z + elif surface == 2: + self.sag2 = z + self.wavefrontError = True + + + def phase_delay(self, x, y, l, theta=None, paraxial=True): + ''' + This function calculates the phase delay introduced by the lens. + + Input: + x, y - Coordinates of the lens + l - Wavelength in millimeters + paraxial - [bool] Apply a paraxial approximation? In the + case of a prism, this means that the prism acts + only as a phase transformation. Diffraction + inside the prism will not take place. + ''' + if theta is None: + theta = self.thetaPrism + + _k = 2 * np.pi / l + _n = self.find_refractive_index(l) + + # Retrieve the sag of the lens surfaces for coordinates x,y + _A1, _A2 = self.get_surface_sag(x,y, theta) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _prismMask = (dist_from_center <= (self.CA/2)).astype(int) + + if paraxial: + # Calculates the phase transformation of this prism. + # Diffraction inside the prism will not take place. + _dxy = self.thickness + _A1 + _A2 + _t_p = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + if self.wavefrontError: + if self.sag_combined is not None: + _t_p *= np.exp(-1j * _k * (_n - 1) * self.sag_combined) + elif self.sag1 is not None: + _t_p *= np.exp(-1j * _k * (_n - 1) * self.sag1) + elif self.sag2 is not None: + _t_p *= np.exp(-1j * _k * (_n - 1) * self.sag2) + + # _t_p *= _prismMask + return _t_p + + if not paraxial: + # Return two different phase transformations + # Option to add surface form error (or wavefront error) + # to each interface. + _t_p_s1 = np.exp(-1j * _k * (_n - 1) * _A1) + _t_p_s2 = np.exp(-1j * _k * (_n - 1) * _A2) + + if self.wavefrontError: + if self.sag1 is not None: + _t_p_s1 *= np.exp(-1j * _k * (_n - 1) * self.sag1) + if self.sag2 is not None: + _t_p_s2 *= np.exp(-1j * _k * (_n - 1) * self.sag2) + if self.sag_combined is not None and self.sag1 is None and self.sag2 is None: + _t_p_s1 *= np.exp(-1j * _k * (_n - 1) * self.sag_combined) + + # _t_p_s1 *= _prismMask + # _t_p_s2 *= _prismMask + return _t_p_s1, _t_p_s2 + + \ No newline at end of file diff --git a/fourierOptics/profiles.py b/fourierOptics/profiles.py new file mode 100644 index 0000000..b1579ff --- /dev/null +++ b/fourierOptics/profiles.py @@ -0,0 +1,73 @@ +# from .__init__ import __data_dir__ +import numpy as np +import os +from astropy.io import fits +from scipy import interpolate + + + +class Profiles: + def __init__(self): + pass + + def top_hat(self, r, n, m, dpix, oversample=1, dx=0, dy=0): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt((i-dx)**2 + (j-dy)**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + + def obscured_top_hat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spider_top_hat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + def gaussian(self, r, n, m, dpix, oversample=1, dx=0, dy=0): + ''' + Creates a grid of n x m points, following a gaussian function + where the radius r is the FWHM of the gaussian shape. + ''' + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt((i-dx)**2 + (j-dy)**2) + return np.exp(-((j-dx)**2 + (i-dy)**2) / r**2) + + + def ELT_pupil(self, n, m, dpix): + # Find data directory + __pkg_dir__ = os.path.dirname(os.path.realpath(__file__)) + __data_dir__ = os.path.join(__pkg_dir__, 'data') + _fits_image = __data_dir__ + '/ELT_M1_pupil_profile.fits' + _ELT_pupil = fits.open(_fits_image)[0].data + _ELT_inner_diam = 38542 # all glass diameter of M1 in mm + _dpix_fits = _ELT_inner_diam / 623 # pixels on the diameter filled with 'glass' + _resx, _resy = np.shape(_ELT_pupil) + _xf = np.linspace(-_resx/2*_dpix_fits, _resx/2*_dpix_fits, _resx) + _yf = np.linspace(-_resy/2*_dpix_fits, _resy/2*_dpix_fits, _resy) + _f_pupil = interpolate.interp2d(_xf, _yf, _ELT_pupil, kind='linear') + _x = np.linspace(-n/2*dpix, n/2*dpix, n) + _y = np.linspace(-m/2*dpix, m/2*dpix, m) + _ELT_pupil = _f_pupil(_x, _y) + return _ELT_pupil \ No newline at end of file diff --git a/fourierOptics/psfFitter.py b/fourierOptics/psfFitter.py new file mode 100644 index 0000000..3f9aca6 --- /dev/null +++ b/fourierOptics/psfFitter.py @@ -0,0 +1,255 @@ +import numpy as np +from scipy import interpolate +from scipy.optimize import leastsq +import types +import scipy.optimize as opt +from scipy.special import j1 + + +class PSFFitter: + ''' + Wrapper around scipy.optimize.curve_fit(), so that point spread functions can be fitted + using some common profiles (e.g. Gaussian, Moffat and Airy pattern profiles). + ''' + def __init__(self, x, y, psfdata): + self.x = x + self.y = y + self.psfdata = psfdata + + + def gaussian_PSF(self, xy, amplitude, x0, y0, sigma_x, sigma_y, theta, offset): + ''' + Elliptical 2D gaussian profile + + Input: + xy - Combined x,y coordinates. x and y should be a meshgrid + amplitude - amplitude of the profile, i.e. maximum of the profile + x0, y0 - shift in x and y of the central peak + sigma_x - width of the profile in x + sigma_y - width of the profile in y + theta - Rotation of the elliptical gaussian profile + offset - noise floor or sky background level + ''' + _x,_y = xy + _x0 = float(x0) + _y0 = float(y0) + _a = (np.cos(theta)**2)/(2*sigma_x**2) + (np.sin(theta)**2)/(2*sigma_y**2) + _b = -(np.sin(2*theta))/(4*sigma_x**2) + (np.sin(2*theta))/(4*sigma_y**2) + _c = (np.sin(theta)**2)/(2*sigma_x**2) + (np.cos(theta)**2)/(2*sigma_y**2) + _g = offset + amplitude*np.exp( - (_a*((_x-_x0)**2) + 2*_b*(_x-_x0)*(_y-_y0) + _c*((_y-_y0)**2))) + return _g.ravel() + + + def fit_gaussian_PSF(self, initialGuess=None): + ''' + Fit a gaussian PSF profile, a Moffat PSF profile and an Airy pattern + An OptimizeWarning is given when the best fit is reached, for some reason; + see also: + https://stackoverflow.com/questions/41725377/why-isnt-curve-fit-able-to-estimate-the-covariance-of-the-parameter-if-the-pa + + Input: + initialGuess - Tuple, containing (amplitude, x0, y0, sigma_x, sigma_y, theta, offset) + ''' + _popt, _pcov = opt.curve_fit(self.gaussian_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess) + return self.gaussian_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov + + + def moffat_PSF(self, xy, amplitude, x0, y0, fwhm1, fwhm2, theta, offset, beta): + ''' + Elliptical moffat distribution, often used to fit a PSF without diffraction rings + Equation taken from http://www.aspylib.com/doc/aspylib_fitting.html + As beta goes to infinity, the moffat profile approaches a gaussian; as mentioned + here https://www.gnu.org/software/gnuastro/manual/html_node/PSF.html + + Input: + xy - Combined x,y coordinates. x and y should be a meshgrid + amplitude - amplitude of the profile, i.e. maximum of the profile + x0, y0 - shift in x and y of the central peak + fwhm1, fwhm2 - width of the profile in perpendicular directions + theta - Rotation of the elliptical moffat profile + offset - noise floor or sky background level + beta - shape parameter of the moffat profile + ''' + _x,_y = xy + _x0 = float(x0) + _y0 = float(y0) + _alpha_1 = fwhm1 / (2 * np.sqrt(2**(1/beta) - 1)) + _alpha_2 = fwhm2 / (2 * np.sqrt(2**(1/beta) - 1)) + _a = (np.cos(theta)/_alpha_1)**2 + (np.sin(theta)/_alpha_2)**2 + _b = (np.sin(theta)/_alpha_1)**2 + (np.cos(theta)/_alpha_2)**2 + _c = np.sin(theta) * np.cos(theta) * (1/(_alpha_1**2) - 1/(_alpha_2**2)) + _m = offset + amplitude/((1 + _a*(_x-_x0)**2 + _b*(_y-_y0)**2 + 2*_c*(_x-_x0)*(_y-_y0))**beta) + return _m.ravel() + + + def fit_moffat_PSF(self, initialGuess=None): + ''' + Fit a Moffat PSF profile + As beta goes to infinity, the moffat profile approaches a gaussian; as mentioned + here https://www.gnu.org/software/gnuastro/manual/html_node/PSF.html + This might raise a RuntimeWarning. + + Input: + initialGuess - Tuple, containing (amplitude, x0, y0, fwhm1, fwhm2, theta, offset, beta) + ''' + _popt, _pcov = opt.curve_fit(self.moffat_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess) + return self.moffat_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov + + + def airy_PSF(self, xy, amplitude, x0, y0, offset, radius): + ''' + 2D Airy pattern with offset included + + Input: + xy - Combined x,y coordinates. x and y should be a meshgrid + amplitude - amplitude of the profile, i.e. maximum of the profile + x0, y0 - shift in x and y of the central peak + offset - noise floor or sky background level + ''' + _x,_y = xy + _x0 = float(x0) + _y0 = float(y0) + _r = np.sqrt((_x-_x0)**2 + (_y-_y0)**2) + _Rz = 1.2196698912665045 + _inp = (np.pi * _r) / (radius/_Rz) + _J1 = j1(_inp) + _fraction = np.nan_to_num(2*_J1 / _inp, nan=1.0) + _a = offset + amplitude * (_fraction)**2 + return _a.ravel() + + + def fit_airy_PSF(self, initialGuess=None): + ''' + Repackaging of scipy's curve_fit to fit an Airy pattern to the data. + No arguments are necessary, but an initial guess of the parameters + is recommended. + + Input: + initialGuess - Tuple, containing (amplitude, x0, y0, offset, radius) + ''' + _popt, _pcov = opt.curve_fit(self.airy_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess) + return self.airy_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov + + + def reference_PSF(self, p, xy, perturbedPSF): + ''' + Residual function used for the mimizer functions in + self.fit_custom_field(), see function below. + ''' + _x0, _y0 = p + _x, _y = xy + _a = self.f_Eref_real(_x - _x0, _y - _y0) + 1j * self.f_Eref_imag(_x - _x0, _y - _y0) + _coupling = self.get_coupling(_a, perturbedPSF) + return 1 - np.abs(_coupling)**2 + + + def fit_custom_field(self, Eref, Eper=None, method='local', niter_success=10, bounds=None, return_coupling=False): + ''' + Fit any custom PSF to the data (self.psfdata). It requires an input + of the custom PSF in the same shape as the data. Then a global + optimization algorithm (scipy.optimization.differential_evolution) + is performed to find the translation parameters; x0 and y0. + + Note: + The current implementation is not able to find other parameters, + such as amplitude, amplitude offset or radius. It assumes that + the custom PSF and data are quite similar in shape. + Also note that what is here called the PSF is actually the complex + field distribution of the PSF, i.e. not the intensity, but the + amplitude and phase information of the PSF. + + Input: + Eref - Custom PSF, including phase information + Eper - optional, perturbed PSF. By default self.psfdata is taken. + method - Optimization strategy to use. Choose between + 'local' + 'global' + 'local' first applies a local search and is very sensitive + to the initial guess. + 'global' searches a global minimum. This is less sensitive + to the initial guess, but requires much more computing + time. + niter_succes- From scipy.optimize.basinhopping: + Stop the run if the global minimum candidate remains the + same for this number of iterations. + bounds - Bounds for the local minimizer + + Returns: + _best_match - Best matched PSF amplitude and phase data. + _popt - Optimum parameters, i.e. translation of the centroid. + _pvar - Variances of the parameters. + _coupling - Optional; coupling factor + + ''' + + assert Eref.shape == self.psfdata.shape, 'The shape of the new (Eper) and reference PSF (Eref) do not match.' + # Set x and y coordinates for interpolation. No 2D meshgrid is necessary. + _x = self.x[0] + _y = self.y[:,0] + + # Check for custom psf data and custom bounds + if Eper is None: + Eper = self.psfdata + print('taken self.psfdata') + + # Create interpolation function to match amplitude and phase to x and y coordinates. + self.f_Eref_real = interpolate.interp2d( _x, _y, Eref.real, kind='cubic') + self.f_Eref_imag = interpolate.interp2d( _x, _y, Eref.imag, kind='cubic') + + # Determining a good initial guess + # Reasonable to assume >0.9 of the maximum amplitude is near the centroid position. + _max_amplitude = np.max(np.abs(Eref)) + _index_i, _index_j = np.where(np.abs(Eref) > (0.9 * _max_amplitude) ) + _x0_guess = np.mean(self.x[_index_i, _index_j]) + _y0_guess = np.mean(self.y[_index_i, _index_j]) + _p0 = [_x0_guess, _y0_guess] + + if method == 'global': + # Arguments for basinhopping function (i.e. global search function) + _min_kwargs = {"args":((_x,_y), Eper), "jac":False} + _popt = opt.basinhopping(self.reference_PSF, _p0, minimizer_kwargs=_min_kwargs, niter_success=niter_success) + _p0 = _popt.x # Set optimal parameters as new first guess. + method = 'local' # Switch to local minimzer to also calculate the uncertainties + + if method == 'local': + # Mimizer function using L-BFGS-B method. + if bounds is None: + bounds = [(_x.min(), _x.max()),(_y.min(), _y.max())] + _res = opt.minimize(self.reference_PSF, _p0, bounds=bounds, args=((_x,_y), Eper) ) + _popt = _res.x + + # Getting the uncertainty in the minimum + _ftol = 2.220446049250313e-09 # ftol assuming L-BFGS-B method in the minimizer. + _hess_inv_i = np.diag(_res.hess_inv.todense()) + _pvar = max(1, abs(_res.fun)) * _ftol * _hess_inv_i + + # Centroid coordinates + _x0_opt, _y0_opt = _popt + _best_match = self.f_Eref_real(_x - _x0_opt, _y - _y0_opt) + 1j * self.f_Eref_imag(_x - _x0_opt, _y - _y0_opt) + + if return_coupling: + _coupling = self.get_coupling(_best_match, Eper) + return _best_match, _popt, _pvar, _coupling + else: + return _best_match, _popt, _pvar + + + def get_coupling(self, Eref, Eper): + ''' + Calculates the coupling integral as given by eq. 6.5 in + Quasi-optical systems by Goldsmith. + + This complex value is a measure of the match between a reference + field and a perturbed field. If the complex coupling factor is c, + then |c|**2 is the degradation of the perturbed field relative to + the reference field, not unlike the Strehl ratio. + + Parameters: + Eref - Array; Reference field + Eper - Array; Perturbed field + + Returns: + _c - Complex float; Coupling factor + ''' + _c = np.sum(Eref * np.conj(Eper))/ np.sqrt(np.sum(np.abs(Eref)**2)*np.sum(np.abs(Eper)**2)) + return _c diff --git a/fourierOptics/wavefrontErrors.py b/fourierOptics/wavefrontErrors.py new file mode 100644 index 0000000..3f3e25f --- /dev/null +++ b/fourierOptics/wavefrontErrors.py @@ -0,0 +1,506 @@ +import numpy as np +import pyfftw +import types +from math import ceil +from scipy.special import factorial, j1 + + +class WavefrontError: + ''' + This class contains methods to describe wavefront errors; + either by means of zernike polynomials (description and fits) + or by means of Power Spectral Density analysis. + ''' + def __init__(self, n, m): + self.n = n # Resolution in x-direction + self.m = m # Resolution in y-direction + self.resolution = (n,m) + + + def calculate_rnm(self,n,m,rho): + ''' + @author: Tibor Agocs + Return an array with the zernike Rnm polynomial calculated at rho points. + + + **ARGUMENTS:** + + === ========================================== + n n order of the Zernike polynomial + m m order of the Zernike polynomial + rho Matrix containing the radial coordinates. + === ========================================== + + .. note:: For rho>1 the returned value is 0 + + .. note:: Values for rho<0 are silently returned as rho=0 + + ''' + + if (n-m)%2!=0: + raise Exception('n-m must be even') + if abs(m)>n: + raise Exception('The following must be true |m|<=n') + + if(n==0 and m==0): + data=np.ones(rho.shape) + return data + _mask = rho > 1 + rho[_mask] = 0 + _Rnm = np.zeros(rho.shape) + _S = (n-np.abs(m))/2 + for s in range (0,int(_S+1)): + _CR =(-1)**s*factorial(n-s)/ \ + (factorial(s)*factorial(-s+(n+np.abs(m))/2)* \ + factorial(-s+(n-np.abs(m))/2)) + _p = _CR * ( rho**(n-2*s) ) + _Rnm = _Rnm + _p + _Rnm[_mask] = 0 + return _Rnm + + + def zernike_polynomial_polar(self,n,m,rho,theta): + ''' + @author: Tibor Agocs + Returns the an array with the Zernike polynomial evaluated in the rho and + theta. + + **ARGUMENTS:** + ===== ========================================== + n n order of the Zernike polynomial + m m order of the Zernike polynomial + rho Matrix containing the radial coordinates. + theta Matrix containing the angular coordinates + ===== ========================================== + + .. note:: For rho>1 the returned value is 0 + + .. note:: Values for rho<0 are silently returned as rho=0 + + **RETURNS** + ===== =========================================== + _Zmn Array with values of the Zernike polynomial + ===== =========================================== + ''' + + _Rnm = self.calculate_rnm(n,m,rho) + + # Normalization constant + _delta_m0 = bool(m==0) + _NC = np.sqrt(2*(n+1)/(1+_delta_m0)) + + if m >= 0: + _Zmn = _NC * _Rnm * np.cos(m*theta) + elif m < 0: + _Zmn = _NC * _Rnm * np.sin(m*theta) + return _Zmn + + + def zernike_polar(self, coefficient, rho, theta, noll=False, rescale='auto'): + ''' + Returns the full surface created from a list of coefficients, + using the Zernike Polynomials. + + rho must be between 0 and 1, if not it will be rescaled, + see also zernikePolynomialPolar() + + **ARGUMENTS:** + ============ ========================================== + coefficient n order of the Zernike polynomial + rho Matrix containing the radial coordinates. + theta Matrix containing the angular coordinates + noll Boolean to indicate to use noll indices + ============ ========================================== + + **RETURNS** + **RETURNS** + ===== =========================================== + _zP Array with values of the Zernike polynomial + ===== =========================================== + ''' + if rescale == 'auto': + if np.max(rho) > np.sqrt(2): + print('Rescaling!') + rho = np.sqrt(2) * rho / np.max(rho) + + _termList = [] + for i in range(len(coefficient)): + _C = coefficient[i] + if noll: + _jTerm = self.convert_noll_to_standard(i+1) + _n,_m = self.i_to_nm(i) + _termList.append( _C * self.zernike_polynomial_polar(_n,_m,rho,theta) ) + _zP = sum(_termList) + return _zP + + + def zernike_cartesian(self, coefficient, x, y, noll=False, addToData=False, **kwargs): + ''' + Converts the cartesian grid x,y into polar + coordinates and returns the data, such that + it can be plotted with e.g. plt.imshow() + + **ARGUMENTS:** + + ============ ========================================== + coefficient n order of the Zernike polynomial + rho Matrix containing the radial coordinates. + theta Matrix containing the angular coordinates + noll Boolean to indicate to use noll indices + ============ ========================================== + + **RETURNS** + ===== =========================================== + _zP Array with values of the Zernike polynomial + ===== =========================================== + ''' + _x0 = (np.max(x) - np.min(x)) / 2 + np.min(x) + _y0 = (np.max(y) - np.min(y)) / 2 + np.min(y) + _rho = np.sqrt(( x - _x0 )**2 + ( y - _y0 )**2 ) + _theta = np.arctan2( ( y - _y0 ) , ( x - _x0 ) ) + _zP = self.zernike_polar(coefficient, _rho, _theta, noll, **kwargs) + if not addToData: + self.data = _zP + else: + self.data += _zP + self.rms = np.sqrt(np.mean(np.asarray(self.data)**2)) + self.ptv = np.abs(np.max(self.data)) + np.abs(np.min(self.data)) + return _zP + + + def i_to_nm(self, i): + ''' + Converts a single index to n and m for the Zernike Polynomials + ''' + n = ceil((-3+np.sqrt(9+8*i))/2) + m = 2 * i - n*(n+2) + return int(n),int(m) + + + def convert_noll_to_standard(self, j): + ''' + + ''' + conversionList = [1, 3, 2, 5, 4, 6, 9, 7, 8, 10, 15, 13, 11, 12, 14, 21, 19, 17, 16, 18, 20, 27, 25, 23, 22, 24, 26, 28, 35, 33, 31, 29, 30, 32, 34, 36, 45, 43, 41, 39, 37, 38, 40, 42, 44, 55, 53, 51, 49, 47, 46, 48, 50, 52, 54, 65, 63, 61, 59, 57, 56, 58, 60, 62, 64, 66, 77, 75, 73, 71, 69, 67, 68, 70, 72, 74, 76, 78, 91, 89, 87, 85, 83, 81, 79, 80, 82, 84, 86, 88, 90, 105, 103, 101, 99, 97, 95, 93, 92, 94, 96, 98, 100, 102, 104, 119, 117, 115, 113, 111, 109, 107, 106, 108, 110, 112, 114, 116, 118, 120] + i = conversionList.index(j) + return i + + + def fit_zernike_coefficients(self, data, x=None, y=None, modes=21): + ''' + Fits the zernike coefficients to a data set, using + that the zernike modes can be treated independently. + The modes are set following standard index notation. + We solve by an analytical least squares fit + Z * A = F + Z.T * Z * A = Z.T * F + A = (Z.T * Z)^-1 * Z.T * F + + **ARGUMENTS:** + + ===== ========================================== + data array; the data that needs fitting + x array; the x-coordinates of the data + y array; the y-coordinates of the data + modes integer; number of modes to be fitted + ===== ========================================== + + **RETURNS** + ===== =========================================== + _A Array with values of the Zernike polynomial + ===== =========================================== + ''' + if x is None and y is None: + y,x = np.mgrid[-1:1:self.n*1j, -1:1:self.m*1j] + _F = data.flatten()[:,np.newaxis] + _x = x.flatten() + _y = y.flatten() + _x0 = (np.max(_x) - np.min(_x)) / 2 + np.min(_x) + _y0 = (np.max(_y) - np.min(_y)) / 2 + np.min(_y) + _rho = np.sqrt( ( _x - _x0 )**2 + ( _y - _y0 )**2 ) + _theta = np.arctan2( ( _y - _y0 ), ( _x - _x0 ) ) + + _Zern = [] + for i in range(modes): + _n, _m = self.i_to_nm(i) + _Znm = self.zernike_polynomial_polar(_n,_m,_rho,_theta) + _Zern.append(_Znm) + _Znm = None + _Zern = np.asarray(_Zern).T + _Zern_transpose = _Zern.T + _pseudo_inv = np.linalg.pinv(np.dot(_Zern_transpose, _Zern)) + _int1 = np.dot(_pseudo_inv, _Zern_transpose) + _A = np.dot(_int1,_F) + self.zernikeCoefficients = _A[:,0] + return _A[:,0] + + + def scale_zernike_coefficients(self, c, rms): + # Scaling the coefficients to some total value + _rms_coeffic = np.sqrt(np.sum( np.asarray(c)**2)) + _scale_factor = rms / _rms_coeffic + _c = (_scale_factor * np.asarray(c)).tolist() + # _rms_coeffic = np.sqrt(np.sum( np.asarray(_c)**2)) + self.zernikeCoefficients = _c + return _c + + + def power_spectral_density(self, s0, p, rho_c, dx, dy, A): + # Lorentzian equation used as PSD function + m = self.m + n = self.n + UmList = [(M - ( m + 1) / 2) / (m * dx) for M in range(m)] + VnList = [(N - ( n + 1) / 2) / (n * dy) for N in range(n)] + VnList, UmList = np.meshgrid(UmList, VnList) + + _rho_mn_list = np.sqrt(UmList**2 + VnList**2) + _h0list = 1 / ( 1 + np.power( ( _rho_mn_list / rho_c ), p) ) + _h0_norm = np.sum(_h0list) + _rho_mn_list = np.asarray(_rho_mn_list).reshape(m,n) + + _PSD = (s0**2 * A) / (_h0_norm * (1 + np.power((_rho_mn_list/rho_c), p))) + self.PSD2D = _PSD + + return _PSD, _rho_mn_list + + + def phase_map(self, phase): + # Uniform distribution of the phase + _phase_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _phase_in[:] = phase + _phase_out = pyfftw.empty_aligned((self.n, self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _phase_in, _phase_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _phaseAngle = np.angle(_phase_out) + + return _phaseAngle + + + def generate_height_map(self, s0, p, rho_c, addToData=False): + ''' + This function generates a surface height distribution based + on a Lorentzian PSD profile. Following the paper by E. Sidick (2009). + + 2020-01-20: Removed "mean=1e-4" from function def. --> Did this do anything? + ''' + + # Calculation of the necessary constants + _dx = 1 / self.m + _dy = 1 / self.n + _A = (self.m * _dx) * (self.n * _dy) + _phase = np.random.uniform(0,1,(self.m,self.m)) + + # Calculating the PSD and phase + _PSD, _rho_mn_list = self.power_spectral_density(s0, p, rho_c, _dx, _dy, _A) + _phiMap = self.phase_map(_phase) + + # Shifting the coordinates for the FFT + _PSD = np.fft.ifftshift(_PSD) + + # Doing the fourier transform + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = np.exp( 1j * _phiMap) * np.sqrt(_A * np.abs(_PSD)) + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # The final height distribution + _hMap = 1 / ( _dx * _dy ) * _out + if not addToData: + self.data = _hMap.real + else: + self.data += _hMap.real + + self.rms = np.sqrt(np.mean(np.asarray(self.data)**2)) + self.ptv = np.abs(np.max(self.data)) + np.abs(np.min(self.data)) + return self.data + + + def scale_rms(self, factor, data=None, filteredDataFlag=False): + # Scale the surface data such that the RMS gets multiplied by factor + _factor = factor ** 2 + if data is None: + if filteredDataFlag: + self.scaledPSD2D = self.filteredPSD2D * _factor + _data = self.filteredData + else: + self.calculate_2D_PSD() + self.scaledPSD2D = self.PSD2D * _factor + _data = self.data + elif data is not None: + self.PSD2D = self.calculate_2D_PSD(data=data) + self.scaledPSD2D = self.PSD2D * _factor + _data = data + + # Fourier transform of the surface profile + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = _data + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _phase = np.angle(_out) + + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.scaledPSD2D)) + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + self.scaledData = _out.real + self.scaledrms = np.sqrt(np.mean(np.asarray(self.scaledData)**2)) + self.scaledptv = np.abs(np.max(self.scaledData)) + np.abs(np.min(self.scaledData)) + + return _out.real + + + def calculate_2D_PSD(self, data=None): + ''' Returns the 2D PSD map ''' + if data is None: + _in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + _in[:] = self.data + _out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128') + elif data is not None: + _n, _m = np.asarray(data).shape + _in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _in[:] = np.asarray(data) + _out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + if data is None: + self.D1 = np.fft.fftshift(_out) + self.PSD2D = np.abs(self.D1)**2 + return self.PSD2D + elif data is not None: + _D1 = np.fft.fftshift(_out) + _PSD2D = np.abs(_D1)**2 + return _PSD2D + + + def calculate_1D_PSD(self, data=None, filteredDataFlag=False, rmax=None): + ''' Returns the azimuthally averaged PSD map ''' + if data is None: + try: + if not filteredDataFlag: + self.PSD1D = self.azimuthal_average(rmax) + else: + self.filteredPSD1D = self.azimuthal_average(filteredDataFlag=filteredDataFlag) + return self.filteredPSD1D + except AttributeError: + self.calculate_2D_PSD(data) + self.PSD1D = self.azimuthal_average(rmax) + return self.PSD1D + elif data is not None: + _PSD2D = self.calculate_2D_PSD(data=data) + _PSD1D = self.azimuthal_average(PSD=_PSD2D, rmax=rmax) + return _PSD1D + + + def set_lowpass_filter(self, r, dpix=1): + ''' Returns a map where all frequencies higher than r have been filtered out ''' + self.calculate_2D_PSD() + # Masking out the parts that fall outside the lens clear aperture + _x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j] + _dist_from_center = np.sqrt(_x**2 + _y**2) * dpix + _mask = (_dist_from_center <= (r)).astype(int) + _maskedPSD2D = self.PSD2D.copy() + _maskedPSD2D *= _mask + _phase = np.angle(np.fft.fft2(self.data)) + self.filteredPSD2D = _maskedPSD2D + self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real + return self.filteredData + + + def set_highpass_filter(self, r, dpix=1): + '''Returns a map where all spatial frequencies lower than r have been filtered out''' + self.calculate_2D_PSD() + # Masking out the parts that fall outside the lens clear aperture + # x,y = np.ogrid[-self.resolution[0]/2:self.resolution[0]/2, -self.resolution[1]/2:self.resolution[1]/2] + _x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j] + _dist_from_center = np.sqrt(_x**2 + _y**2) * dpix + _mask = (_dist_from_center <= (r)).astype(int) + _maskedPSD2D = self.PSD2D.copy() + _maskedPSD2D *= (1-_mask) + _phase = np.angle(np.fft.fft2(self.data)) + self.filteredPSD2D = _maskedPSD2D + self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real + return self.filteredData + + + def set_bandpass_filter(self, rmin, rmax, dpix=1): + '''Returns a map where all spatial frequencies lower than r2 and higher than r1 have been filtered out''' + assert rmax > rmin, 'rmax < rmin, not physical!' + self.calculate_2D_PSD() + # Masking out the parts that fall outside the lens clear aperture + # x,y = np.ogrid[-self.resolution[0]/2:self.resolution[0]/2, -self.resolution[1]/2:self.resolution[1]/2] + _x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j] + _dist_from_center = np.sqrt(_x**2 + _y**2) * dpix + _mask_1 = (_dist_from_center <= (rmax)).astype(int) + _mask_2 = (_dist_from_center >= (rmin)).astype(int) + _maskedPSD2D = self.PSD2D.copy() + _maskedPSD2D *= (_mask_1 & _mask_2) + _phase = np.angle(np.fft.fft2(self.data)) + self.filteredPSD2D = _maskedPSD2D + self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real + return self.filteredData + + + def azimuthal_average(self, PSD=None, center=None, filteredDataFlag=False, rmax=None): + """ + Calculate the azimuthally averaged radial profile. + + image - The 2D image + center - The [x,y] pixel coordinates used as the center. The default is + None, which then uses the center of the image (including + fracitonal pixels). + + """ + + # Calculate the indices from the image + _y, _x = np.indices(self.resolution) + if not center: + center = np.array([(_x.max()-_x.min())/2.0, (_y.max()-_y.min())/2.0]) + + _r = np.hypot(_x - center[0], _y - center[1]) + + # Get sorted radii + _ind = np.argsort(_r.flat) + _r_sorted = _r.flat[_ind] + + if not filteredDataFlag and PSD is None: + _PSD = self.PSD2D + + elif PSD is None: + _PSD = self.filteredPSD2D + + elif PSD is not None: + _PSD = PSD + + if rmax == True: + _PSD[ _r > _x.max()/2 ] = np.nan + elif rmax: + _PSD[ _r > rmax ] = np.nan + + _i_sorted = _PSD.flat[_ind] + + # Get the integer part of the radii (bin size = 1) + _r_int = _r_sorted.astype(int) + + # Find all pixels that fall within each radial bin. + _deltar = _r_int[1:] - _r_int[:-1] # Assumes all radii represented + _rind = np.where(_deltar)[0] # location of changed radius + _nr = _rind[1:] - _rind[:-1] # number of radius bin + + # Cumulative sum to figure out sums for each radius bin + _csim = np.cumsum(_i_sorted, dtype=float) + _tbin = _csim[_rind[1:]] - _csim[_rind[:-1]] + _radial_prof = _tbin / _nr + + return _radial_prof + + def generate_sine_pattern(self, ampx, ampy, perx, pery): + _x, _y = np.mgrid[-1:1:self.n*1j, -1:1:self.m*1j] + _z = ampx * np.sin(2*np.pi*_x*perx/2) + ampy * np.sin(2 * np.pi * _y * pery/2) + return _z + + def generate_tilt(self, x, y, tx, ty): + _z = y * np.tan(np.deg2rad(tx)) + x * np.tan(np.deg2rad(ty)) + return _z diff --git a/test cases (standalone)/fourierOptics_01_diffracting_aperture.py b/test cases (standalone)/fourierOptics_01_diffracting_aperture.py new file mode 100644 index 0000000..1e2389e --- /dev/null +++ b/test cases (standalone)/fourierOptics_01_diffracting_aperture.py @@ -0,0 +1,735 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate +import time + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + # _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=dpix) + _fy_l = np.fft.fftfreq(_m, d=dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + _y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m] + if outputLimits: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*dpix, _n/2*dpix, _n) + _y = np.linspace(-_m/2*dpix, _m/2*dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputLimits[0],outputLimits[1],_n) + _yi = np.linspace(outputLimits[0],outputLimits[1],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beamRadius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + else: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freqArray(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phaseFactorExponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real + + + def gaussianBeamFit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + + + + +class profiles: + def __init__(self): + pass + + def topHat(self, r, n, m, dpix, oversample=1): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + def fundamentalMode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beamRadius(z) + _R = self.curvature(z) + _phi0 = self.phaseShift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + def curvature(self, z): + return z + self.z_c **2 / z + + def beamRadius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + def phaseShift(self, z): + return np.arctan(z/self.z_c) + + def relativePower(self, r, z): + return np.exp(-2 * (r / self.beamRadius(z))**2) + + + +class lensObject: + ''' + The lens object describes any spherical lens and find the phase delay + for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None): + self.glass = glass + assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog' + self.R1 = R1 # mm + self.R2 = R2 # mm + self.thickness = thickness # mm + self.CA = CA # mm + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.type = 'Spherical lens' + if not f: + self.f = self.findFocalLength() + else: + self.f = f + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + def findRefractiveIndex(self, _l): + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* _l**2)/(_l**2 - _a[1]) + (_a[2] * _l**2) / (_l**2 - _a[3]) + (_a[4] * _l**2) / (_l**2 - _a[5]) + 1) + return _n + + def findFocalLength(self): + _n = self.findRefractiveIndex(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + def phaseDelayFunction(self, x, y, l, paraxial=False): + if not paraxial: + _A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + _A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + _dxy = self.thickness - _A1 + _A2 + if paraxial: + _dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2) + _k = 2 * np.pi / 30e-3 + _n = self.findRefractiveIndex(self.referenceWavelength) + _t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA)).astype(int) + _t_l *= _lensMask + return _t_l + + + +class mirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + +class parabolicMirror(mirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'Parabolic mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class sphericalMirror(mirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.CA = CA + self.type = 'Spherical mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class freeformMirror(mirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'User def. mirror' + + def phaseDelayFunction(self, l): + _k = 2 * np.pi / l + _t_m = np.exp(-1j * _k * 2 * self.z) + return _t_m + + + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + def print_lens_Data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _wf = U_in + + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i] + _wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number() + + if _F < 5: + # Fresnel propagation + _wf, x, y = self.fresnel_propagation() + elif _F >= 5: + # Angular spectrum (p2p) propagation + _wf, x, y = self.angular_spectrum_propagation() + + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + + + + +if __name__ == '__main__': + plt.rc('font', family='serif') + plt.rc('axes', labelsize='large') + + # Start time counter + start = time.time() + + # Pick a wavelength + l = 30e-3 + + # Initiate Fourier Optics + fou = FourierOptics(l=l) + + # Pick sampling parameters + dpix = 1/50*l + n, m = 2001, 2001 + print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix)) + + # Initiate the profiles class and pick a profile (tophat, obscured tophat or spider) + prof = profiles() + # A = prof.spiderTopHat(250, 100, 2001, 2001, dpix=1, oversample=1) + A = prof.topHat(4*l, n, m, dpix=dpix, oversample=1) + + # Alternatively, create a gaussian profile + # gauss = GaussianBeam(0.3, l*1000) + # A = gauss.fundamentalMode(0,n,m,np.max(x)) + + # Create an input grid + x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m)) + + # The input distribution gets a curved wavefront after the lens + U_in = A + + + Fresnel = False + P2P = not Fresnel + + oC = [x.min(), x.max(), y.min(), y.max()] + # Performing the propagation using Fresnel approximation to the focal plane. + if Fresnel: + savename = 'fourierOptics_01_diffracting_aperture_fresnel.pdf' + Exyz,_,_ = fou.FresnelPropagationFFTW(U_in, x, y, dz=4*l, oversample=None, show=True, save=savename, outputCoordinates=oC) + if P2P: + savename = 'fourierOptics_01_diffracting_aperture_p2p.pdf' + Exyz,_,_ = fou.planeWavePropagationFFTW(A, dz=4*l, dpix=dpix, oversample=None, show=True, save=savename) + + # Plot a cross section of the new field (useful to better see the diffraction rings) + fig = plt.figure() + ax = fig.add_subplot(111) + ax.grid(True, alpha=0.3) + ax.semilogy(np.linspace(np.min(x), np.max(x),n), np.abs(Exyz[1000,:]**2), 'r-') + ax.set_xlabel('x (mm)') + ax.set_ylabel('Intensity') + ax.tick_params(axis ='both', direction='in', which='both') + if Fresnel: + plt.savefig('fourierOptics_01_diffracting_aperture_fresnel_slice.pdf', bbox_inches='tight', transparent=True) + if P2P: + plt.savefig('fourierOptics_01_diffracting_aperture_p2p_slice.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves. + if True: + Ics = [] + Ics_x = [] + Isum = [] + xmax = [] + zList = np.linspace(1,10.1,18).tolist() + xin, yin = x, y + + for i in zList: + title = f"{zList.index(i)+1:03d}" + print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r") + + # Propagate the wavefront + if Fresnel: + Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=None, show=False, save=False)#, scaleInputArray=(n,m)) + if P2P: + Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i*l, oversample=None, show=False, save=False, outputCoordinates=oC) + + # Process the results by taking a slice of the data + Isum.append(np.sum(np.abs(Exyz)**2)) + Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2)) + Ics_x.append(x.real[0]) + y_ext = [y.real[0,0],y.real[-1,0]] + + # Reset the data + x = None + y = None + + # Plotting all slices + fig = plt.figure(figsize=(8,4)) + ax = fig.add_subplot(211) + ax.tick_params(axis ='both', direction='in') + + # Plotting the square root of the amplitude for better contrast + im = ax.imshow(np.asarray(Ics).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]]) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + ax.set_ylim([-0.2, 0.2]) + + # Plotting the intensity relative to the input + ax2 = fig.add_subplot(212) + ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-') + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + ax2.set_xlabel('Propagation distance ($\\times \lambda$)') + ax2.tick_params(axis ='both', direction='in') + ax2.set_ylabel('Intensity') + ax2.set_xlim([np.min(zList), np.max(zList)]) + ax2.grid(True, alpha=0.3) + + # Saving the figure? Then show. + plt.tight_layout() + # plt.savefig('fresnel_cross.pdf', bbox_inches='tight') + # plt.savefig('fresnel_cross.png', bbox_inches='tight') + if P2P: + plt.savefig('fourierOptics_01_diffracting_aperture_p2p_cross.pdf', bbox_inches='tight', transparent=True) + if Fresnel: + plt.savefig('fourierOptics_01_diffracting_aperture_fresnel_cross.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # Closing down the script and stopping the timer. + end = time.time() + print('The program took {:.3f} seconds!'.format((end-start))) + + + diff --git a/test cases (standalone)/fourierOptics_025_single_lens_system.py b/test cases (standalone)/fourierOptics_025_single_lens_system.py new file mode 100644 index 0000000..7631059 --- /dev/null +++ b/test cases (standalone)/fourierOptics_025_single_lens_system.py @@ -0,0 +1,729 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate +import time + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + # _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=dpix) + _fy_l = np.fft.fftfreq(_m, d=dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + _y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m] + if outputLimits: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*dpix, _n/2*dpix, _n) + _y = np.linspace(-_m/2*dpix, _m/2*dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputLimits[0],outputLimits[1],_n) + _yi = np.linspace(outputLimits[0],outputLimits[1],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beamRadius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + else: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freqArray(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phaseFactorExponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real + + + def gaussianBeamFit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + + + + +class profiles: + def __init__(self): + pass + + def topHat(self, r, n, m, dpix, oversample=1): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + def fundamentalMode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beamRadius(z) + _R = self.curvature(z) + _phi0 = self.phaseShift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + def curvature(self, z): + return z + self.z_c **2 / z + + def beamRadius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + def phaseShift(self, z): + return np.arctan(z/self.z_c) + + def relativePower(self, r, z): + return np.exp(-2 * (r / self.beamRadius(z))**2) + + + +class lensObject: + ''' + The lens object describes any spherical lens and find the phase delay + for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None): + self.glass = glass + assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog' + self.R1 = R1 # mm + self.R2 = R2 # mm + self.thickness = thickness # mm + self.CA = CA # mm + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.type = 'Spherical lens' + if not f: + self.f = self.findFocalLength() + else: + self.f = f + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + def findRefractiveIndex(self, _l): + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* _l**2)/(_l**2 - _a[1]) + (_a[2] * _l**2) / (_l**2 - _a[3]) + (_a[4] * _l**2) / (_l**2 - _a[5]) + 1) + return _n + + def findFocalLength(self): + _n = self.findRefractiveIndex(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + def phaseDelayFunction(self, x, y, l, paraxial=False): + if not paraxial: + _A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + _A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + _dxy = self.thickness - _A1 + _A2 + if paraxial: + _dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2) + _k = 2 * np.pi / 30e-3 + _n = self.findRefractiveIndex(self.referenceWavelength) + _t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA)).astype(int) + _t_l *= _lensMask + return _t_l + + + +class mirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + +class parabolicMirror(mirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'Parabolic mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class sphericalMirror(mirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.CA = CA + self.type = 'Spherical mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class freeformMirror(mirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'User def. mirror' + + def phaseDelayFunction(self, l): + _k = 2 * np.pi / l + _t_m = np.exp(-1j * _k * 2 * self.z) + return _t_m + + + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + def print_lens_Data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _wf = U_in + + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i] + _wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number() + + if _F < 5: + # Fresnel propagation + _wf, x, y = self.fresnel_propagation() + elif _F >= 5: + # Angular spectrum (p2p) propagation + _wf, x, y = self.angular_spectrum_propagation() + + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + + + + +if __name__ == '__main__': + plt.rc('font', family='serif') + plt.rc('axes', labelsize='large') + + # Start time counter + start = time.time() + + # Pick a wavelength + l = 30e-3 + + # Initiate Fourier Optics + fou = FourierOptics(l=l) + + # Pick sampling parameters + dpix = 1/50 + n, m = 2001, 2001 + print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix)) + + # Initiate the profiles class and pick a profile (tophat, obscured tophat or spider) + prof = profiles() + # A = prof.spiderTopHat(250, 100, 2001, 2001, dpix=1, oversample=1) + A = prof.topHat(2.5, n, m, dpix=dpix, oversample=1) + + # Alternatively, create a gaussian profile + # gauss = GaussianBeam(0.3, l*1000) + # A = gauss.fundamentalMode(0,n,m,np.max(x)) + + # Create an input grid + x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m)) + + # Start an optical train object. Add lenses and propagate. Currently not in use. + # LDE = OpticalTrain() + + # Create a lens with a focal length ~100 mm, with a clear aperture radius of 4 mm and thickness of 1 mm. + L1 = lensObject('N-BK7', 50, 1e99, 1, 4, name='L1') + print('Focal length of lens {} is {:.1f} mm.'.format(L1.name, L1.f)) + + # Calculate the phase delay introduced by the lens + t_l = L1.phaseDelayFunction(x,y,1.35e-6, paraxial=False) + + # The input distribution gets a curved wavefront after the lens + U_in = A * t_l + + + # Performing the propagation using Fresnel approximation to the focal plane. + oC = [-5, 5, -5, 5] + savename = 'simple_lens.pdf' + Exyz,_,_ = fou.FresnelPropagationFFTW(U_in, x, y, dz=L1.f, oversample=None, show=True, save=savename, outputCoordinates=oC) + + # Plot a cross section of the new field (useful to better see the diffraction rings) + fig = plt.figure() + ax = fig.add_subplot(111) + ax.grid(True, alpha=0.3) + ax.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[1000,:]**2), 'r-') + ax.set_xlabel('x (mm)') + ax.set_ylabel('Intensity') + ax.tick_params(axis ='both', direction='in', which='both') + plt.savefig('simple_lens_slice.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves. + if True: + Ics = [] + Ics_x = [] + Isum = [] + xmax = [] + zList = np.linspace(20,220,20).tolist() + xin, yin = x, y + + for i in zList: + title = f"{zList.index(i)+1:03d}" + print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r") + + # Propagate the wavefront + # Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=1, show=False, save=False)#, scaleInputArray=(n,m)) + Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i, oversample=None, show=False, save=False, outputCoordinates=oC) + + # Process the results by taking a slice of the data + Isum.append(np.sum(np.abs(Exyz)**2)) + Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2)) + Ics_x.append(x.real[0]) + y_ext = [y.real[0,0],y.real[-1,0]] + + # Reset the data + x = None + y = None + + # Plotting all slices + fig = plt.figure(figsize=(8,4)) + ax = fig.add_subplot(211) + ax.tick_params(axis ='both', direction='in') + + # Plotting the square root of the amplitude for better contrast + im = ax.imshow(np.sqrt(np.asarray(Ics)).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]]) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + + # Plotting the intensity relative to the input + ax2 = fig.add_subplot(212) + ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-') + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + ax2.set_xlabel('Propagation distance (mm)') + ax2.tick_params(axis ='both', direction='in') + ax2.set_ylabel('Amplitude') + ax2.set_xlim([np.min(zList), np.max(zList)]) + ax2.grid(True, alpha=0.3) + + # Saving the figure? Then show. + plt.tight_layout() + # plt.savefig('fresnel_cross.pdf', bbox_inches='tight') + # plt.savefig('fresnel_cross.png', bbox_inches='tight') + plt.savefig('simple_lens_cross.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # Closing down the script and stopping the timer. + end = time.time() + print('The program took {:.3f} seconds!'.format((end-start))) + + + diff --git a/test cases (standalone)/fourierOptics_02_telescope.py b/test cases (standalone)/fourierOptics_02_telescope.py new file mode 100644 index 0000000..427c9d7 --- /dev/null +++ b/test cases (standalone)/fourierOptics_02_telescope.py @@ -0,0 +1,771 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate +import time + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + # _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=dpix) + _fy_l = np.fft.fftfreq(_m, d=dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + _y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m] + if outputLimits: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*dpix, _n/2*dpix, _n) + _y = np.linspace(-_m/2*dpix, _m/2*dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputLimits[0],outputLimits[1],_n) + _yi = np.linspace(outputLimits[0],outputLimits[1],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beamRadius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + else: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + # _im = _ax.imshow(np.sqrt(np.abs(_E_out_ip)), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + # _fig.colorbar(_im, ax=_ax, label='$\\sqrt{Amplitude}$') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freqArray(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phaseFactorExponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real + + + def gaussianBeamFit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + + + + +class profiles: + def __init__(self): + pass + + def topHat(self, r, n, m, dpix, oversample=1): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + def fundamentalMode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beamRadius(z) + _R = self.curvature(z) + _phi0 = self.phaseShift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + def curvature(self, z): + return z + self.z_c **2 / z + + def beamRadius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + def phaseShift(self, z): + return np.arctan(z/self.z_c) + + def relativePower(self, r, z): + return np.exp(-2 * (r / self.beamRadius(z))**2) + + + +class lensObject: + ''' + The lens object describes any spherical lens and find the phase delay + for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None): + self.glass = glass + assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog' + self.R1 = R1 # mm + self.R2 = R2 # mm + self.thickness = thickness # mm + self.CA = CA # mm + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.type = 'Spherical lens' + if not f: + self.f = self.findFocalLength() + else: + self.f = f + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + def findRefractiveIndex(self, _l): + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* _l**2)/(_l**2 - _a[1]) + (_a[2] * _l**2) / (_l**2 - _a[3]) + (_a[4] * _l**2) / (_l**2 - _a[5]) + 1) + return _n + + def findFocalLength(self): + _n = self.findRefractiveIndex(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + def phaseDelayFunction(self, x, y, l, paraxial=False): + if not paraxial: + _A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + _A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + _dxy = self.thickness - _A1 + _A2 + if paraxial: + _dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2) + _k = 2 * np.pi / 30e-3 + _n = self.findRefractiveIndex(self.referenceWavelength) + _t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA)).astype(int) + _t_l *= _lensMask + return _t_l + + + +class mirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + +class parabolicMirror(mirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'Parabolic mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + _t_m = np.exp(1j * _k * 2 * _dz) + return _t_m + + + +class sphericalMirror(mirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.CA = CA + self.type = 'Spherical mirror' + + def phaseDelayFunction(self, x, y, l): + # _k = 2 * np.pi / l + # _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + # _t_m = np.exp(1j * _k * 2 * _dz) + # return _t_m + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + + # Masking out the parts that fall outside the mirror clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _mirrorMask = (dist_from_center <= (self.CA/2)).astype(int) + _t_m *= _mirrorMask + return _t_m + + +class freeformMirror(mirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'User def. mirror' + + def phaseDelayFunction(self, l): + _k = 2 * np.pi / l + _t_m = np.exp(1j * _k * 2 * self.z) + return _t_m + + + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + def print_lens_Data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _wf = U_in + + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i] + _wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number() + + if _F < 5: + # Fresnel propagation + _wf, x, y = self.fresnel_propagation() + elif _F >= 5: + # Angular spectrum (p2p) propagation + _wf, x, y = self.angular_spectrum_propagation() + + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + + + + +if __name__ == '__main__': + plt.rc('font', family='serif') + plt.rc('axes', labelsize='large') + + # Start time counter + start = time.time() + + # Pick a wavelength + l = 30e-3 + + # Initiate Fourier Optics + fou = FourierOptics(l=l) + + # Pick sampling parameters + dpix = 8 + n, m = 4001, 4001 + print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix)) + + # Initiate the profiles class and pick a profile (tophat, obscured tophat or spider) + prof = profiles() + A = prof.topHat(4000, n, m, dpix=dpix, oversample=1) + # A = prof.spiderTopHat(4000, 1000, n, m, dpix=dpix, oversample=1) + # A = prof.obscuredTopHat(4000, 1000, n, m, dpix=dpix, oversample=1) + + # Alternatively, create a gaussian profile + # gauss = GaussianBeam(0.3, l*1000) + # A = gauss.fundamentalMode(0,n,m,np.max(x)) + + # Create an input grid + x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m)) + + # Start an optical train object. Add lenses and propagate. Currently not in use. + # LDE = OpticalTrain() + + # Create a mirror with a focal length 64 m, with a clear aperture radius of 4 m. + M1 = parabolicMirror(dx=0, dy=0, name='M1', CA=8000, f=64000) + print('Focal length of mirror {} is {:.1f} mm.'.format(M1.name, M1.f)) + + # Calculate the phase delay introduced by the mirror + t_l = M1.phaseDelayFunction(x,y,l) + + # The input distribution gets a curved wavefront after the mirror + U_in = A * t_l + + plt.imshow(np.abs(U_in)) + plt.colorbar() + plt.savefig('telescope_mask.pdf', bbox_inches='tight', transparent=True) + plt.show(block=False) + + # Performing the propagation using Fresnel approximation to the focal plane. + # oC = [-0.001, 0.001, -0.001, 0.001] + oC = [-2, 2, -2, 2] + # savename = 'fourierOptics_02_telescope_fresnel.pdf' + Exyz,xout,yout = fou.FresnelPropagationFFTW(U_in, x, y, dz=M1.f, oversample=None, show=True, save=False, outputCoordinates=oC) + + # Plot a cross section of the new field (useful to better see the diffraction rings) + fig = plt.figure() + ax = fig.add_subplot(111) + ax.grid(True, alpha=0.3) + ax.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[int((n-1)/2),:]**2), 'r-') + ax.set_xlabel('x (mm)') + ax.set_ylabel('Intensity') + ax.tick_params(axis ='both', direction='in', which='both') + # plt.savefig('fourierOptics_02_telescope_slice.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # Combined plot + # Plotting the results + _fig = plt.figure(figsize=(12,3)) + _ax = _fig.add_subplot(131) + _im = _ax.imshow(np.abs(Exyz), cmap='hot', extent=oC) + _ax.set_xlim([oC[0], oC[1]]) + _ax.set_ylim([oC[2], oC[3]]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(132) + _im2 = _ax2.imshow(np.angle(Exyz), cmap='hot', extent=oC) + _ax2.set_xlim([oC[0], oC[1]]) + _ax2.set_ylim([oC[2], oC[3]]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + _ax3 = _fig.add_subplot(133) + _ax3.grid(True, alpha=0.3) + _ax3.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[int((n-1)/2),:]**2), 'r-') + _ax3.set_xlabel('x (mm)') + _ax3.set_ylabel('Intensity') + _ax3.tick_params(axis ='both', direction='in', which='both') + plt.tight_layout() + plt.savefig('fourierOptics_02_telescope_complete.pdf', bbox_inches='tight', transparent=True) + + plt.show() + + # The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves. + if False: + Ics = [] + Ics_x = [] + Isum = [] + xmax = [] + zList = np.linspace(20,220,20).tolist() + xin, yin = x, y + + for i in zList: + title = f"{zList.index(i)+1:03d}" + print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r") + + # Propagate the wavefront + # Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=1, show=False, save=False)#, scaleInputArray=(n,m)) + Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i, oversample=None, show=False, save=False, outputCoordinates=oC) + + # Process the results by taking a slice of the data + Isum.append(np.sum(np.abs(Exyz)**2)) + Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2)) + Ics_x.append(x.real[0]) + y_ext = [y.real[0,0],y.real[-1,0]] + + # Reset the data + x = None + y = None + + # Plotting all slices + fig = plt.figure(figsize=(8,4)) + ax = fig.add_subplot(211) + ax.tick_params(axis ='both', direction='in') + + # Plotting the square root of the amplitude for better contrast + im = ax.imshow(np.sqrt(np.asarray(Ics)).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]]) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + + # Plotting the intensity relative to the input + ax2 = fig.add_subplot(212) + ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-') + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + ax2.set_xlabel('Propagation distance (mm)') + ax2.tick_params(axis ='both', direction='in') + ax2.set_ylabel('Intensity') + ax2.set_xlim([np.min(zList), np.max(zList)]) + ax2.grid(True, alpha=0.3) + + # Saving the figure? Then show. + plt.tight_layout() + # plt.savefig('fresnel_cross.pdf', bbox_inches='tight') + # plt.savefig('fresnel_cross.png', bbox_inches='tight') + plt.show() + + + # Closing down the script and stopping the timer. + end = time.time() + print('The program took {:.3f} seconds!'.format((end-start))) + + + diff --git a/test cases (standalone)/fourierOptics_03_verification.py b/test cases (standalone)/fourierOptics_03_verification.py new file mode 100644 index 0000000..7f2a618 --- /dev/null +++ b/test cases (standalone)/fourierOptics_03_verification.py @@ -0,0 +1,775 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + # _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=dpix) + _fy_l = np.fft.fftfreq(_m, d=dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + _y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m] + if outputLimits: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*dpix, _n/2*dpix, _n) + _y = np.linspace(-_m/2*dpix, _m/2*dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputLimits[0],outputLimits[1],_n) + _yi = np.linspace(outputLimits[0],outputLimits[1],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beamRadius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + else: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freqArray(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phaseFactorExponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real + + + def gaussianBeamFit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + + + + +class profiles: + def __init__(self): + pass + + def topHat(self, r, n, m, dpix, oversample=1): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + def fundamentalMode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beamRadius(z) + _R = self.curvature(z) + _phi0 = self.phaseShift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + def curvature(self, z): + return z + self.z_c **2 / z + + def beamRadius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + def phaseShift(self, z): + return np.arctan(z/self.z_c) + + def relativePower(self, r, z): + return np.exp(-2 * (r / self.beamRadius(z))**2) + + + +class lensObject: + ''' + The lens object describes any spherical lens and find the phase delay + for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None): + self.glass = glass + assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog' + self.R1 = R1 # mm + self.R2 = R2 # mm + self.thickness = thickness # mm + self.CA = CA # mm + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.type = 'Spherical lens' + if not f: + self.f = self.findFocalLength() + else: + self.f = f + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + def findRefractiveIndex(self, _l): + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* _l**2)/(_l**2 - _a[1]) + (_a[2] * _l**2) / (_l**2 - _a[3]) + (_a[4] * _l**2) / (_l**2 - _a[5]) + 1) + return _n + + def findFocalLength(self): + _n = self.findRefractiveIndex(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + def phaseDelayFunction(self, x, y, l, paraxial=False): + if not paraxial: + _A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + _A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + _dxy = self.thickness - _A1 + _A2 + if paraxial: + _dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2) + _k = 2 * np.pi / 30e-3 + _n = self.findRefractiveIndex(self.referenceWavelength) + _t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA)).astype(int) + _t_l *= _lensMask + return _t_l + + + +class mirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + +class parabolicMirror(mirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'Parabolic mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class sphericalMirror(mirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.CA = CA + self.type = 'Spherical mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class freeformMirror(mirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'User def. mirror' + + def phaseDelayFunction(self, l): + _k = 2 * np.pi / l + _t_m = np.exp(-1j * _k * 2 * self.z) + return _t_m + + + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + def print_lens_Data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _wf = U_in + + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i] + _wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number() + + if _F < 5: + # Fresnel propagation + _wf, x, y = self.fresnel_propagation() + elif _F >= 5: + # Angular spectrum (p2p) propagation + _wf, x, y = self.angular_spectrum_propagation() + + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + +def FGB_1D(w0,l,z,x): + k = 2 * np.pi / l + z = z + 1e-16 + w = w0 * np.sqrt( 1 + ( (l*z) / (np.pi*w0**2) )**2) + R = z + 1/z * ( np.pi * w0**2 / l)**2 + phi = np.arctan2( (l * z) , (np.pi * w0**2) ) + + norm = (2./ np.pi)**(.25) * (1. / w0**2)**(.5) + beam = np.exp( -x**2/w**2 - (1j * k * z) - ((1j*np.pi*x**2)/(l*R)) + 1j*phi/2) + + return beam * norm + +if __name__ == '__main__': + plt.rc('font', family='serif') + plt.rc('axes', labelsize='large') + + import time + start = time.time() + l = 30e-3 + # l = 1e-6 + fou = FourierOptics(l=l) + dpix = l/6 + n = int(500 / dpix * l) + m = int(500 / dpix * l) + n, m = 3001, 3001 + print (n,m) + prof = profiles() + # A = prof.topHat(4*l, 2001, 2001, dpix=dpix, oversample=1) + + # Create an input grid + x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m)) + + w0_init = 4*l + z = 400*l + wg = w0_init * np.sqrt( 1 + ( (l*z) / (np.pi*w0_init**2) )**2) + # rmax_init = 4 * w0_init + # rmax_init = 1000/8*l + + xGauss = np.linspace(-(n/2)*dpix, (n/2)*dpix, n) + rmax_init = np.max(xGauss) + # A = GaussianBeam(w0_init, l).fundamentalMode(z, n, m, rmax_init) + A0 = FGB_1D(w0_init, l, 0, xGauss) + A0 /= np.sqrt(np.sum(np.abs(A0)**2)) + A = FGB_1D(wg, l, z, xGauss) + + A /= np.sqrt(np.sum(np.abs(A)**2)) + + A2D = GaussianBeam(w0_init, l).fundamentalMode(z, n, m, rmax_init) + A2D /= np.sqrt(np.sum(np.abs(A2D[int(n/2)+1,:])**2)) + + A2D0 = GaussianBeam(w0_init, l).fundamentalMode(0, n, m, rmax_init) + + Ap2p,xp2p,_ = fou.planeWavePropagationFFTW(A2D0, dz=z, dpix=dpix, show=False, save=False) + Ap2p /= np.sqrt(np.sum(np.abs(Ap2p[int(n/2),:])**2)) + + oC = [xGauss.min(), xGauss.max(), xGauss.min(), xGauss.max()] + + # savename= 'FourierOptics_03_verification_fresnel.pdf' + savename=None + Afren,xfren,_ = fou.FresnelPropagationFFTW(A2D0, x,y, dz=z, show=True, save=savename, outputCoordinates=oC) + Afren /= np.sqrt(np.sum(np.abs(Afren[int(n/2),:])**2)) + # Afren = np.conj(Afren) + + norm_fac = np.max(np.abs(A2D)[int(n/2),:]) + + fig = plt.figure() + ax = fig.add_subplot(111) + # ax.plot(np.abs(A0), label='z=0, analytical') + # ax.plot(np.abs(A)/norm_fac, label='z=%i, analytical' %(int(z/l))) + ax.plot(np.abs(A2D)[int(n/2),:]/norm_fac, 'r--', label='z=%i$\lambda$, analytical' %(int(z/l))) + ax.plot(np.abs(Ap2p)[int(n/2),:]/norm_fac, label='z=%i$\lambda$, plane to plane' %(int(z/l))) + ax.plot(np.abs(Afren)[int(n/2),:]/norm_fac, label='z=%i$\lambda$, Fresnel' %(int(z/l))) + ax.grid(alpha=0.3) + ax.set_xlabel('Location [pixel no.]') + ax.set_ylabel('Normalized amplitude') + ax.legend() + plt.savefig('FourierOptics_03_verification_amplitude.pdf', bbox_inches='tight', transparent=True) + + fig2 = plt.figure(figsize=(12,3)) + # ax1 = fig2.add_subplot(151) + # ax1.plot(np.angle(A0), label='z=0, analytical') + # ax1.set_title('z=0, analytical') + # ax2 = fig2.add_subplot(131) + # ax2.plot(np.angle(A), label='z=%i, analytical' %(int(z/l))) + # ax2.set_title('z=%i, analytical' %(int(z/l))) + ax3 = fig2.add_subplot(131) + ax3.plot(np.angle(A2D)[int(n/2),:], label='z=%i$\lambda$, analytical' %(int(z/l))) + ax3.set_title('z=%i$\lambda$, Gaussian beam' %(int(z/l))) + ax3.set_ylabel('Phase') + ax3.set_xlabel('Location [pixel no.]') + ax3.grid(True, alpha=0.3) + ax4 = fig2.add_subplot(132) + ax4.plot(np.angle(Ap2p)[int(n/2),:], label='z=%i$\lambda$, plane to plane' %(int(z/l))) + ax4.set_title('z=%i$\lambda$, Plane to plane' %(int(z/l))) + ax4.set_ylabel('Phase') + ax4.set_xlabel('Location [pixel no.]') + ax4.grid(True, alpha=0.3) + ax5 = fig2.add_subplot(133) + ax5.plot(np.angle(Afren)[int(n/2),:], label='z=%i$\lambda$, Fresnel' %(int(z/l))) + ax5.set_title('z=%i$\lambda$, Fresnel' %(int(z/l))) + ax5.set_ylabel('Phase') + ax5.set_xlabel('Location [pixel no.]') + ax5.grid(True, alpha=0.3) + # ax1.grid(alpha=0.3) + # ax6 = fig2.add_subplot(166) + # ax6.legend(ax=ax6) + plt.savefig('FourierOptics_03_verification_phase.pdf', bbox_inches='tight', transparent=True) + plt.tight_layout() + plt.show() + + + # set_trace() + # Afren,_,_ = fou.FresnelPropagationFFTW(A2D, dz=400*l, dpix=dpix, oversample=1, show=True, save=False, outputCoordinates=oC) + + gauss = GaussianBeam(w0_init, l) + # A = prof.topHat(4*l, 2001, 2001, dpix=dpix, oversample=1) + A = gauss.fundamentalMode(0,n,m,rmax_init) + if False: + + Ics = [] + Ics_x = [] + Isum = [] + lList = np.linspace(1,201,10).tolist() + for i in lList: + title = f"{lList.index(i):03d}" + print (' Calculating {0}/{1}'.format(title, f"{len(lList):03d}"), end="\r") + # Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, show=False, save=False)#, scaleInputArray=(n,m)) + Exyz, x, y = fou.FresnelPropagationFFTW(A, dz=i*l, dpix=dpix, show=False, save=False, outputCoordinates=oC) + # Exyz = gauss.fundamentalMode(i*l, n, m, rmax_init) + # y, x = np.mgrid[-rmax_init:rmax_init:n*1j,-rmax_init:rmax_init:n*1j] + Isum.append(np.sum(np.abs(Exyz)**2)) + Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2)) + Ics_x.append(x.real[0]) + y_ext = [y.real[0,0],y.real[-1,0]] + print(y_ext) + Exyz = None + x = None + y = None + + fig = plt.figure(figsize=(8,4)) + ax = fig.add_subplot(211) + ax.tick_params(axis ='both', direction='in') + im = ax.imshow(np.asarray(Ics).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[lList[0],lList[-1],y_ext[0],y_ext[1]]) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + # ax.set_ylim([-2.5e-4,2.5e-4]) + ax2 = fig.add_subplot(212) + ax2.plot(lList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-') + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + ax2.set_xlabel('Propagation distance [$\lambda$]') + ax2.tick_params(axis ='both', direction='in') + ax2.set_ylabel('Intensity') + ax2.set_xlim([np.min(lList), np.max(lList)]) + ax2.grid(True, alpha=0.3) + + # ax3 = fig.add_subplot(133) + # ax3.set_ylabel('Integrated intensity') + # ax3.set_xlabel('Propagation distance [$\lambda$]') + # ax3.grid(True, alpha=0.3) + # ax3.tick_params(axis ='both', direction='in') + # ax3.plot(Isum) + plt.tight_layout() + # plt.savefig('fresnel_cross.pdf', bbox_inches='tight') + # plt.savefig('fresnel_cross.png', bbox_inches='tight') + plt.show() + + # E_xyz,_,_ = fou.planeWavePropagationFFTW(A, dz=4*l, dpix=dpix, oversample=1, show=True, save=False, outputLimits=(-0.4*1e-3,0.4*1e-3))#, scaleInputArray=(n,m)) + # Exyz,_,_ = fou.FresnelPropagationFFTW(A, dz=4*l, dpix=dpix, oversample=1, show=True, save=False)#, scaleInputArray=(n,m)) + end = time.time() + print((end-start)*1000) + # set_trace() + + diff --git a/test cases (standalone)/fourierOptics_04_simple_lens_system.py b/test cases (standalone)/fourierOptics_04_simple_lens_system.py new file mode 100644 index 0000000..0b99f2d --- /dev/null +++ b/test cases (standalone)/fourierOptics_04_simple_lens_system.py @@ -0,0 +1,720 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate +import time + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + # _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=dpix) + _fy_l = np.fft.fftfreq(_m, d=dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + _y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m] + if outputLimits: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*dpix, _n/2*dpix, _n) + _y = np.linspace(-_m/2*dpix, _m/2*dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputLimits[0],outputLimits[1],_n) + _yi = np.linspace(outputLimits[0],outputLimits[1],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beamRadius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + else: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _f_ip_real = interpolate.interp2d(_x.real, _y.real, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x.real, _y.real, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freqArray(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phaseFactorExponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real + + + def gaussianBeamFit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + + + + +class profiles: + def __init__(self): + pass + + def topHat(self, r, n, m, dpix, oversample=1): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + def fundamentalMode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beamRadius(z) + _R = self.curvature(z) + _phi0 = self.phaseShift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + def curvature(self, z): + return z + self.z_c **2 / z + + def beamRadius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + def phaseShift(self, z): + return np.arctan(z/self.z_c) + + def relativePower(self, r, z): + return np.exp(-2 * (r / self.beamRadius(z))**2) + + + +class lensObject: + ''' + The lens object describes any spherical lens and find the phase delay + for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None): + self.glass = glass + assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog' + self.R1 = R1 # mm + self.R2 = R2 # mm + self.thickness = thickness # mm + self.CA = CA # mm + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.type = 'Spherical lens' + if not f: + self.f = self.findFocalLength() + else: + self.f = f + self.type = 'Thin paraxial lens' + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + def findRefractiveIndex(self, _l): + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* _l**2)/(_l**2 - _a[1]) + (_a[2] * _l**2) / (_l**2 - _a[3]) + (_a[4] * _l**2) / (_l**2 - _a[5]) + 1) + return _n + + def findFocalLength(self): + _n = self.findRefractiveIndex(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + def phase_delay(self, x, y, l, paraxial=False): + if self.type == 'Thin paraxial lens': + assert self.type=='Thin paraxial lens' and paraxial, 'Can not calculate non-paraxial phase delay on paraxial lens!' + + _k = 2 * np.pi / l + _n = self.findRefractiveIndex(self.referenceWavelength) + + if not paraxial: + _A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + _A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + _dxy = self.thickness - _A1 + _A2 + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy) + if paraxial: + _t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2)) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA)).astype(int) + _t_l *= _lensMask + return _t_l + + + +class mirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + +class parabolicMirror(mirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'Parabolic mirror' + + def phase_delay(self, x, y, l): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class sphericalMirror(mirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.CA = CA + self.type = 'Spherical mirror' + + def phase_delay(self, x, y, l): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class freeformMirror(mirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'User def. mirror' + + def phase_delay(self, l): + _k = 2 * np.pi / l + _t_m = np.exp(-1j * _k * 2 * self.z) + return _t_m + +class opticalSurface: + def __init__(self, mask=None, name=None): + self.type = 'Dummy surface' + if mask: + self.x, self.y, self.mask = mask + self.type = 'Mask' + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + + pass + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + + def print_lens_Data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + def fresnel_number(self, a, z, l): + return a**2 / (z * l) + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _fou = FourierOptics(l=l) + + _wf = U_in + + oC = [x.min(), x.max(), y.min(), y.max()] + show = False + _wf, x, y = _fou.FresnelPropagationFFTW(_wf, x, y, dz=self.sequentialDistance[0], oversample=None, show=show, save=False, outputCoordinates=oC) + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + + for _lens in self.opticalTrain[:-1]: # Except for image plane + + title = f"{self.opticalTrain[:-1].index(_lens)+1:02d}" + print (' Calculating lens {0} of {1}'.format(title, f"{len(self.opticalTrain[:-1]):02d}"), end="\r") + + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i+1] + _wf *= _lens.phase_delay(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number(_lens.CA, _dz, l) + _F = 1 + + + if _F < 5: + # Fresnel propagation + if _lens == self.opticalTrain[-2]: + oC = [-2, 2, -2, 2] + show=True + _wf, x, y = _fou.FresnelPropagationFFTW(_wf, x, y, dz=self.sequentialDistance[_i+1], oversample=None, show=show, save=False, outputCoordinates=oC) + elif _F >= 5: + print('Using Plane Wave!') + # Angular spectrum (p2p) propagation + # _wf, x, y = self.plane_wave_propagation() + + pass + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + + + + + +if __name__ == '__main__': + plt.rc('font', family='serif') + plt.rc('axes', labelsize='large') + + # Start time counter + start = time.time() + + # Pick a wavelength + l = 30e-3 + + # Initiate Fourier Optics + fou = FourierOptics(l=l) + + # Pick sampling parameters + dpix = 1/20 + n, m = 4001, 4001 + print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix)) + + # Initiate the profiles class and pick a profile (tophat, obscured tophat or spider) + A = profiles().topHat(25, n, m, dpix=dpix, oversample=1) + + + # Create an input grid + x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m)) + + # Start an optical train object. Add lenses and propagate. Currently not in use. + LDE = OpticalTrain() + # Create a lens with D=50 mm, f=200 mm + L1 = lensObject('N-BK7', None, None, 0, 25, f=200, name='L1') + L2 = lensObject('N-BK7', None, None, 0, 12.5, f=-100, name='L2') + L3 = lensObject('N-BK7', None, None, 0, 25, f=200, name='L3') + IMG = opticalSurface(name='Image plane') + + L0 = lensObject('N-BK7', None, None, 0, 25, f=400, name='Input intiater lens') + t_l = L0.phase_delay(x,y,l, paraxial=True) + U_in = A * t_l + set_trace() + # U_in = Fresnel propagation to L0.f + + oC = [x.min(), x.max(), y.min(), y.max()] + Exyz, x1, y1 = fou.FresnelPropagationFFTW(U_in, x, y, dz=L0.f, oversample=None, show=False, save=False, outputCoordinates=oC) + U_in = Exyz + + + LDE.add_object(L1, 400) + LDE.add_object(L2, 200) + LDE.add_object(L3, 200) + LDE.add_object(IMG, 400) + + LDE.print_lens_Data() + LDE.propagate_system(U_in, x, y, l) + # fig = plt.figure() + # ax1 = fig.add_subplot(121) + # ax2 = fig.add_subplot(122) + # im1 = ax1.imshow(np.abs(wf), cmap='hot', extent=[x.min(), x.max(), y.min(), y.max()]) + # im2 = ax2.imshow(np.angle(wf), cmap='hot', extent=[x.min(), x.max(), y.min(), y.max()]) + # plt.colorbar(im1, ax=ax1) + # plt.colorbar(im2, ax=ax2) + # plt.show() + set_trace() + + + + + + # Closing down the script and stopping the timer. + end = time.time() + print('The program took {:.3f} seconds!'.format((end-start))) + + + diff --git a/test cases (standalone)/fourierOptics_25_single_lens_system.py b/test cases (standalone)/fourierOptics_25_single_lens_system.py new file mode 100644 index 0000000..7631059 --- /dev/null +++ b/test cases (standalone)/fourierOptics_25_single_lens_system.py @@ -0,0 +1,729 @@ +#!/usr/bin/env Python3 +import numpy as np +from pdb import set_trace +import matplotlib.pyplot as plt +from cmath import sqrt as csqrt +from scipy import interpolate +from scipy.optimize import leastsq +from matplotlib.ticker import FormatStrFormatter +from matplotlib.colors import LogNorm +import pyfftw +import glassCatalog +from tabulate import tabulate +import time + + +class FourierOptics: + def __init__(self, l): + self.l = l # Wavelength + + + def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + if oversample: + _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + A = _Aos + + _n, _m = np.shape(A) + + # A at z=0 is then + _E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array + # _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs + _IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) + _Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + + # E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz) + _fx_l = np.fft.fftfreq(_n, d=dpix) + _fy_l = np.fft.fftfreq(_m, d=dpix) + _fx, _fy = np.meshgrid(_fx_l, _fy_l) + _kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array + _phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array + + # Calculating the spatial frequencies in k-space + _kx = np.fft.fftshift( _fx * 2 * np.pi ) + _ky = np.fft.fftshift( _fy * 2 * np.pi ) + + _E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _A_in[:] = _Az0 * _phaseFactor + + _FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj2() + _E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center + _E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy. + + # Coordinates of E_xy0 + _y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m] + if outputLimits: + # Decide only use for plotting or can be used for output as well? + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _x = np.linspace(-_n/2*dpix, _n/2*dpix, _n) + _y = np.linspace(-_m/2*dpix, _m/2*dpix, _m) + _f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear') + _xi = np.linspace(outputLimits[0],outputLimits[1],_n) + _yi = np.linspace(outputLimits[0],outputLimits[1],_m) + _E_out_ip_real = _f_ip_real(_xi, _yi) + _E_out_ip_imag = _f_ip_imag(_xi, _yi) + _E_xyz = _E_out_ip_real + _E_out_ip_imag*1j + _y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m] + + + _E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)] + + + + if show or save: + fig = plt.figure(figsize=(8,3)) + ax = fig.add_subplot(121) + im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0) + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + fig.colorbar(im, ax=ax, label='Normalized intensity') + ax2 = fig.add_subplot(122) + im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent) + ax2.set_xlabel('x [mm]') + ax2.set_ylabel('y [mm]') + fig.colorbar(im2, ax=ax2) + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight') + if show: + plt.show() + elif not show: + plt.close() + + return np.array([_E_xyz, _x, _y]) + + + def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'): + ''' + Propagation through the Fresnel approximation, which is valid in many different situations. + A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent + converges to 1 for large z values in any case. + + Input parameters: + A - [array] (Complex) Input field + dpix - [float] pixel scale at which the calculations are done + dz - [float] propagation distance + oversample - [bool] Apply padding on the outside of A, increasing the size of A + by the given factor. + show - [bool] Show the resulting amplitude and phase map after propagation + save - [bool] Save the figure + scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation. + outputCoordinates - [list] list the corners of the desired output coordinate plane. + Order: [xmin, xmax, ymin, ymax] + If 'auto': the output coordinates will be determined + automatically. + + Output: + E_out - [array] (Complex) Output field distribution + x, y - [array] Output coordinates + ''' + assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.' + assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.' + A = np.conj(A) + + if scaleInputArray: + A = np.asarray(A) + _n, _m = A.shape + _nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m) + A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear') + A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear') + + _a, _b = scaleInputArray + _nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b) + A_out_ip_real = A_ip_real(_nx, _my) + A_out_ip_imag = A_ip_imag(_nx, _my) + A_out_ip = A_out_ip_real + A_out_ip_imag*1j + A = np.rint(A_out_ip_real) + + # if oversample: + # _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample)) + # _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A + # A = _Aos + + _n, _m = np.shape(A) + _k = 2 * np.pi / self.l + + # Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale. + _xi, _eta = xin, yin + + # Calculate the pixel size from the xi, eta plane + _dpix = np.mean(np.diff(_xi)) + if _dpix < 1e-99: + _dpix = np.mean(np.diff(_xi.T)) + + # Gaussian test: use 8*w0 radius for FFT: + if outputCoordinates == 'auto': + _w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A)) + _gauss = GaussianBeam(_w0_fit, self.l) + _halfwidth = 8 * _gauss.beamRadius(dz)[0] + _xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane + _yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane + else: + _xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane + _yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane + + + # The quadratic phase exponential of the input wavefront + _phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2)) + + # Complete input + _U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _U_in[:] = A * _phaseComponent + + # Output of the FFT evaluated at frequency fx = x / (lambda * z) + _U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128') + _FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',)) + _FFTW_Obj() + _U_out = np.fft.fftshift(_U_out) + + # Evaluate frequencies: + _fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix)) + _fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix)) + _x = _fx * self.l * dz + _y = _fy * self.l * dz + _x2, _y2 = np.meshgrid(_x, _y) + + + # Final output and frequencies + _pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2)) + _E_out = _pre_amble * _U_out + + # Normalization of the output values + _IntensityNormFactor = np.sum(np.abs(A)**2) + _E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m)) + + # Performing interpolation to get to the required final plane, done for real and imaginary part. + _f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear') + _f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear') + _E_out_ip_real = _f_ip_real(_xip, _yip) + _E_out_ip_imag = _f_ip_imag(_xip, _yip) + _E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j + _E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max()))) + _E_out_ip = np.conj(_E_out_ip) + + if show: + # Plotting the results + _fig = plt.figure(figsize=(8,3)) + _ax = _fig.add_subplot(121) + _im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax.set_xlim([_xip.min(), _xip.max()]) + _ax.set_ylim([_yip.min(), _yip.max()]) + _ax.set_xlabel('x [mm]') + _ax.set_ylabel('y [mm]') + _fig.colorbar(_im, ax=_ax, label='Amplitude') + _ax2 = _fig.add_subplot(122) + _im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()]) + _ax2.set_xlim([_xip.min(), _xip.max()]) + _ax2.set_ylim([_yip.min(), _yip.max()]) + _ax2.set_xlabel('x [mm]') + _ax2.set_ylabel('y [mm]') + _fig.colorbar(_im2, ax=_ax2, label='Phase') + plt.tight_layout() + if save: + plt.savefig(save, bbox_inches='tight', transparent=True) + plt.show() + + _x, _y = np.meshgrid(_xip, _yip) + return np.array([_E_out_ip, _x, _y]) + + + def freqArray(self, A, dpix): + n, m = np.shape(A) + + fy, fx = np.mgrid[0:m, 0:n] + + fx = fx.flatten() + fy = fy.flatten() + + for i in range(len(fx)): + fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix) + fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix) + + fx = fx.reshape(n,m) + fy = fy.reshape(n,m) + + return fx, fy + + + def phaseFactorExponent(self, fx, fy, l): + k = 2*np.pi / l + kx = np.asarray(2*np.pi * fx) + ky = np.asarray(2*np.pi * fy) + K_squared = (kx**2 + ky**2) + kA = np.ones(K_squared.shape) * k + K_1 = np.sqrt((kA**2-K_squared).astype('complex128')) + kz = K_1.real + -1j*K_1.imag + return kz + + + def minimize(self, a, dz, x, y, og_data): + ''' + Residual function used for fitting the gaussian beam to the input distribution. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + og_data - [array] input distribution to be fitted. + ''' + return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real + + + def gaussianBeamFit(self, a, dz, x, y): + ''' + Function used to fit the Gaussian beam to an input plane. Very similar + to the GaussianBeam class below, but modified to allow a best fit. + Input: + a - [float] Guess for w0 + dz - [float] Propagation distance, should be known + x,y - [array] meshgrid of the x and y coordinates. + ''' + _w0 = a + _z = dz + _r = np.sqrt(x**2 + y**2) + if _z != 0: + _z_c = np.pi * _w0**2 / self.l + _w = _w0 * np.sqrt(1 + (_z/_z_c)**2) + _R = _z + _z_c **2 / _z + _phi0 = np.arctan(_z/_z_c) + _k = 2 * np.pi / self.l + _results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0) + elif _z == 0: + _results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 ) + return _results + + + + + + +class profiles: + def __init__(self): + pass + + def topHat(self, r, n, m, dpix, oversample=1): + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r)).astype(int) + return _Anew + + def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + return _Anew.astype(int) + + + def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1): + ''' + Creates a grid of n x m points, following a top hat function + where all points inside a radius r is 1 and 0 otherwise. + ''' + assert r1 > r2, "r2 < r1, change input" + j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample] + n *= oversample + m *= oversample + dist_from_center = np.sqrt(i**2 + j**2) + _Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2)) + _Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1)) + _Anew = _Anew & ~_Aspider + return _Anew + + + +class GaussianBeam: + def __init__(self, w0, l): + # Confocal distance + self.z_c = np.pi * w0**2 / l + + # Far-field divergence angle + self.theta_0 = l / (np.pi * w0) + self.l = l + self.w0 = w0 + self.fwhm = 1.18 * self.theta_0 + + def fundamentalMode(self, z, n, m, rmax): + y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j] + r = np.sqrt(x**2 + y**2) + if z != 0: + _w = self.beamRadius(z) + _R = self.curvature(z) + _phi0 = self.phaseShift(z) + _k = 2 * np.pi / self.l + return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0) + elif z == 0: + return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 ) + + def curvature(self, z): + return z + self.z_c **2 / z + + def beamRadius(self, z): + return self.w0 * np.sqrt(1 + (z/self.z_c)**2) + + def phaseShift(self, z): + return np.arctan(z/self.z_c) + + def relativePower(self, r, z): + return np.exp(-2 * (r / self.beamRadius(z))**2) + + + +class lensObject: + ''' + The lens object describes any spherical lens and find the phase delay + for all light traveling through it. + The refractive index, of course, depends on the wavelength. + ''' + def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None): + self.glass = glass + assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog' + self.R1 = R1 # mm + self.R2 = R2 # mm + self.thickness = thickness # mm + self.CA = CA # mm + self.dispersionCoefficients = glassCatalog.catalog.get(self.glass) + self.referenceWavelength = 1.35 # micron + self.type = 'Spherical lens' + if not f: + self.f = self.findFocalLength() + else: + self.f = f + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + def findRefractiveIndex(self, _l): + _a = self.dispersionCoefficients + _n = np.sqrt( (_a[0]* _l**2)/(_l**2 - _a[1]) + (_a[2] * _l**2) / (_l**2 - _a[3]) + (_a[4] * _l**2) / (_l**2 - _a[5]) + 1) + return _n + + def findFocalLength(self): + _n = self.findRefractiveIndex(self.referenceWavelength) + _f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2) + return 1/_f_recip + + def phaseDelayFunction(self, x, y, l, paraxial=False): + if not paraxial: + _A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) ) + _A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) ) + _dxy = self.thickness - _A1 + _A2 + if paraxial: + _dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2) + _k = 2 * np.pi / 30e-3 + _n = self.findRefractiveIndex(self.referenceWavelength) + _t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy) + + # Masking out the parts that fall outside the lens clear aperture + dist_from_center = np.sqrt(x**2 + y**2) + _lensMask = (dist_from_center <= (self.CA)).astype(int) + _t_l *= _lensMask + return _t_l + + + +class mirrorObject: + ''' + Describes any mirror. No wavelength dependence exist. + Coatings are not considered. + ''' + def __init__(self, dx, dy, name=None): + self.dx = dx + self.dy = dy + if name: + assert type(name)==str, 'No string for a name.' + self.name = name + else: + self.name = str(self) + + +class parabolicMirror(mirrorObject): + def __init__(self, f, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.f = f + self.CA = CA + self.type = 'Parabolic mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = (x**2 + y**2) / (4 * self.f) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class sphericalMirror(mirrorObject): + def __init__(self, R, CA, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.R = R + self.CA = CA + self.type = 'Spherical mirror' + + def phaseDelayFunction(self, x, y, l): + _k = 2 * np.pi / l + _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) ) + _t_m = np.exp(-1j * _k * 2 * _dz) + return _t_m + + + +class freeformMirror(mirrorObject): + def __init__(self, x, y, z, dx=None, dy=None, name=None): + mirrorObject.__init__(self, dx=dx, dy=dy, name=name) + self.x = x + self.y = y + self.z = z + self.type = 'User def. mirror' + + def phaseDelayFunction(self, l): + _k = 2 * np.pi / l + _t_m = np.exp(-1j * _k * 2 * self.z) + return _t_m + + + + +class OpticalTrain: + ''' + The main class where an optical system is build up sequentially. + ''' + def __init__(self): + self.opticalTrain = [] + self.sequentialDistance = [] + + def add_object(self, obj, dist): + ''' + Adding a lens or mirror object to the opticalTrain + obj The lens or mirror object + dist The distance from the the previous object. + ''' + assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.' + self.opticalTrain.append(obj) + self.sequentialDistance.append(float(dist)) + + def print_lens_Data(self): + ''' + Print the data of the current opticalTrain. + ''' + _tableData = [] + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _distances = self.sequentialDistance + _distances.append('-') + _tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ]) + print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface'])) + + + def propagate_system(self, U_in, x, y, l, output=None, paraxial=True): + # We need to have an input distribution + # We need to determine what propagation method is required + # Call the objects in the self.opticalTrain sequentially + # Call the subsequent phaseDelay + if output == 'complete': + _wf_list = [] + _x_list = [] + _y_list = [] + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + _wf = U_in + + for _lens in self.opticalTrain: + _i = self.opticalTrain.index(_lens) + _dz = self.sequentialDistance[_i] + _wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial) + + # Perform Fresnel number test: + _F = self.fresnel_number() + + if _F < 5: + # Fresnel propagation + _wf, x, y = self.fresnel_propagation() + elif _F >= 5: + # Angular spectrum (p2p) propagation + _wf, x, y = self.angular_spectrum_propagation() + + if output == 'complete': + _wf_list.append(_wf) + _x_list.append(x) + _y_list.append(y) + + if output == 'Complete': + return np.array([_wf_list, _x_list, _y_list]) + + else: + return np.array([_wf, x, y]) + + + + + +if __name__ == '__main__': + plt.rc('font', family='serif') + plt.rc('axes', labelsize='large') + + # Start time counter + start = time.time() + + # Pick a wavelength + l = 30e-3 + + # Initiate Fourier Optics + fou = FourierOptics(l=l) + + # Pick sampling parameters + dpix = 1/50 + n, m = 2001, 2001 + print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix)) + + # Initiate the profiles class and pick a profile (tophat, obscured tophat or spider) + prof = profiles() + # A = prof.spiderTopHat(250, 100, 2001, 2001, dpix=1, oversample=1) + A = prof.topHat(2.5, n, m, dpix=dpix, oversample=1) + + # Alternatively, create a gaussian profile + # gauss = GaussianBeam(0.3, l*1000) + # A = gauss.fundamentalMode(0,n,m,np.max(x)) + + # Create an input grid + x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m)) + + # Start an optical train object. Add lenses and propagate. Currently not in use. + # LDE = OpticalTrain() + + # Create a lens with a focal length ~100 mm, with a clear aperture radius of 4 mm and thickness of 1 mm. + L1 = lensObject('N-BK7', 50, 1e99, 1, 4, name='L1') + print('Focal length of lens {} is {:.1f} mm.'.format(L1.name, L1.f)) + + # Calculate the phase delay introduced by the lens + t_l = L1.phaseDelayFunction(x,y,1.35e-6, paraxial=False) + + # The input distribution gets a curved wavefront after the lens + U_in = A * t_l + + + # Performing the propagation using Fresnel approximation to the focal plane. + oC = [-5, 5, -5, 5] + savename = 'simple_lens.pdf' + Exyz,_,_ = fou.FresnelPropagationFFTW(U_in, x, y, dz=L1.f, oversample=None, show=True, save=savename, outputCoordinates=oC) + + # Plot a cross section of the new field (useful to better see the diffraction rings) + fig = plt.figure() + ax = fig.add_subplot(111) + ax.grid(True, alpha=0.3) + ax.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[1000,:]**2), 'r-') + ax.set_xlabel('x (mm)') + ax.set_ylabel('Intensity') + ax.tick_params(axis ='both', direction='in', which='both') + plt.savefig('simple_lens_slice.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves. + if True: + Ics = [] + Ics_x = [] + Isum = [] + xmax = [] + zList = np.linspace(20,220,20).tolist() + xin, yin = x, y + + for i in zList: + title = f"{zList.index(i)+1:03d}" + print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r") + + # Propagate the wavefront + # Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=1, show=False, save=False)#, scaleInputArray=(n,m)) + Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i, oversample=None, show=False, save=False, outputCoordinates=oC) + + # Process the results by taking a slice of the data + Isum.append(np.sum(np.abs(Exyz)**2)) + Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2)) + Ics_x.append(x.real[0]) + y_ext = [y.real[0,0],y.real[-1,0]] + + # Reset the data + x = None + y = None + + # Plotting all slices + fig = plt.figure(figsize=(8,4)) + ax = fig.add_subplot(211) + ax.tick_params(axis ='both', direction='in') + + # Plotting the square root of the amplitude for better contrast + im = ax.imshow(np.sqrt(np.asarray(Ics)).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]]) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + + # Plotting the intensity relative to the input + ax2 = fig.add_subplot(212) + ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-') + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) + ax2.set_xlabel('Propagation distance (mm)') + ax2.tick_params(axis ='both', direction='in') + ax2.set_ylabel('Amplitude') + ax2.set_xlim([np.min(zList), np.max(zList)]) + ax2.grid(True, alpha=0.3) + + # Saving the figure? Then show. + plt.tight_layout() + # plt.savefig('fresnel_cross.pdf', bbox_inches='tight') + # plt.savefig('fresnel_cross.png', bbox_inches='tight') + plt.savefig('simple_lens_cross.pdf', bbox_inches='tight', transparent=True) + plt.show() + + + # Closing down the script and stopping the timer. + end = time.time() + print('The program took {:.3f} seconds!'.format((end-start))) + + + diff --git a/test cases (standalone)/glassCatalog.py b/test cases (standalone)/glassCatalog.py new file mode 100644 index 0000000..f50d65b --- /dev/null +++ b/test cases (standalone)/glassCatalog.py @@ -0,0 +1,12 @@ +#!/usr/bin/env Python3 + +# Pyton dictionary with some glass types + +catalog = { + + 'N-BK7' : [1.039612120E+00, 6.000698670E-03, 2.317923440E-01, 2.001791440E-02, 1.010469450E+00, 1.035606530E+02 ], + 'SiO2' : [0.6961663, 0.0684043, 0.4079426, 0.1162414, 0.8974794, 9.896161 ], + 'F2' : [1.345333590E+00, 9.977438710E-03, 2.090731760E-01, 4.704507670E-02, 9.373571620E-01, 1.118867640E+02 ], + 'S-FPL51' : [1.170105050E+00, 6.162039240E-03, 4.757107830E-02, 2.633728760E-02, 7.638324450E-01, 1.418826420E+02 ], + 'S-LAH71' : [1.982800310E+00, 1.189874560E-02, 3.167584500E-01, 5.271560010E-02, 2.444726460E+00, 2.132206970E+02 ], +} \ No newline at end of file