├── .gitignore ├── LICENSE.md ├── MIT 16.853 Intro to Link Budgets.pdf ├── OLBtools ├── Z136.py ├── __init__.py ├── beams.py ├── reporting.py └── scintillation.py ├── README.md ├── Z136.1.verification.py ├── example_1.py ├── example_2.py ├── example_3.py ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Installer logs 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | 27 | # Unit test / coverage reports 28 | .tox/ 29 | .coverage 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Rope 43 | .ropeproject 44 | 45 | # Django stuff: 46 | *.log 47 | *.pot 48 | 49 | # Sphinx documentation 50 | docs/_build/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright © 2007 Free Software Foundation, Inc. 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 8 | 9 | This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 10 | 11 | ## 0. Additional Definitions. 12 | 13 | As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. 14 | 15 | "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. 16 | 17 | An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. 18 | 19 | A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". 20 | 21 | The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. 22 | 23 | The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 24 | 25 | ## 1. Exception to Section 3 of the GNU GPL. 26 | 27 | You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 28 | 29 | ## 2. Conveying Modified Versions. 30 | 31 | If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: 32 | 33 | 1. under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or 34 | 2. under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 35 | 36 | ## 3. Object Code Incorporating Material from Library Header Files. 37 | 38 | The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: 39 | 40 | 1. Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. 41 | 2. Accompany the object code with a copy of the GNU GPL and this license document. 42 | 43 | ## 4. Combined Works. 44 | 45 | You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: 46 | 47 | 1. Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. 48 | 2. Accompany the Combined Work with a copy of the GNU GPL and this license document. 49 | 3. For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. 50 | 4. Do one of the following: 51 | 1. Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 52 | 2. Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. 53 | 5. Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4.4.1., the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4.4.2., you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 54 | 55 | ## 5. Combined Libraries. 56 | 57 | You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: 58 | 59 | 1. Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. 60 | 2. Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 61 | 62 | ## 6. Revised Versions of the GNU Lesser General Public License. 63 | 64 | The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 65 | 66 | Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. 67 | 68 | If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. 69 | -------------------------------------------------------------------------------- /MIT 16.853 Intro to Link Budgets.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MIT-STARLab/Optical-Link-Budget/6da100e88fa34084341a655f28dff41c9b5276ea/MIT 16.853 Intro to Link Budgets.pdf -------------------------------------------------------------------------------- /OLBtools/Z136.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | import numpy as np 3 | 4 | import warnings 5 | warnings.warn("This library does not come with any garanties of accuracy, please refer to Z136.1 directly for any safety-related or important determinations") 6 | 7 | 8 | def C_A(wavelength:np.ndarray): 9 | C_a = 10**(0.002*(wavelength*1e9-700)) 10 | return np.maximum(np.minimum(C_a,5.0),1.0) 11 | 12 | def C_B(wavelength:np.ndarray): 13 | C_b = 10**(0.02*(wavelength*1e9-450)) 14 | return np.maximum(C_b,1.0) 15 | 16 | def C_C(wavelength:np.ndarray): 17 | C_c_1 = 10**(0.018*(wavelength*1e9-1150)) 18 | C_c_2 = 8+10**(0.040*(wavelength*1e9-1250)) 19 | mask = (wavelength < 1250e-9) 20 | C_c = C_c_2*(1-mask) + np.minimum(C_c_1,C_c_2)*mask 21 | return np.maximum(C_c,1.0) 22 | 23 | def T_1(wavelength:np.ndarray): 24 | T_1 = 10*10**(0.02*(wavelength*1e9-450)) 25 | return T_1 26 | 27 | def K_lambda(wavelength:np.ndarray): 28 | K_l = 10**(0.01*(1400-wavelength*1e9)) 29 | return K_l 30 | 31 | 32 | 33 | def ocular_MPE_point_source_NIR(wavelength:np.ndarray=1064e-9,exposure_time:np.ndarray=30000): 34 | '''Based on Z136.1 table 5c, return MPE in W.m-2 35 | Valid between 700 and 1400 nm''' 36 | retina_MPE = 0 37 | cornea_MPE = 0 38 | 39 | C_a = C_A(wavelength) 40 | C_c = C_C(wavelength) 41 | K_l = K_lambda(wavelength) 42 | 43 | mask_0700_1050 = ( 700e-9 <= wavelength)*(wavelength < 1050e-9) 44 | mask_1050_1200 = (1050e-9 <= wavelength)*(wavelength < 1200e-9) 45 | mask_1200_1400 = (1200e-9 <= wavelength)*(wavelength < 1400e-9) 46 | mask_1050_1400 = mask_1050_1200 + mask_1200_1400 47 | 48 | mask_100fs_10ps = ( 1e-13 <= exposure_time)*(exposure_time < 1e-11) 49 | mask_10ps_5us = ( 1e-11 <= exposure_time)*(exposure_time < 5e-06) 50 | mask_5us_10s = ( 5e-06 <= exposure_time)*(exposure_time < 10) 51 | mask_10ps_13us = ( 1e-11 <= exposure_time)*(exposure_time < 13e-06) 52 | mask_13us_10s = (13e-06 <= exposure_time)*(exposure_time < 10) 53 | mask_10ps_1ms = ( 1e-11 <= exposure_time)*(exposure_time < 1e-03) 54 | mask_1ms_4s = ( 1e-03 <= exposure_time)*(exposure_time < 4) 55 | mask_4s_10s = ( 4 <= exposure_time)*(exposure_time < 10) 56 | mask_above_10s = (exposure_time >= 10) 57 | 58 | retina_MPE += mask_0700_1050 * mask_100fs_10ps * 1e-7 59 | retina_MPE += mask_0700_1050 * mask_10ps_5us * 2e-7*C_a 60 | retina_MPE += mask_0700_1050 * mask_5us_10s * 1.8e-3*C_a*exposure_time**(0.75) 61 | 62 | retina_MPE += mask_1050_1400 * mask_100fs_10ps * 1e-7*C_c 63 | retina_MPE += mask_1050_1400 * mask_10ps_13us * 2e-6*C_c 64 | retina_MPE += mask_1050_1400 * mask_13us_10s * 9e-3*C_c*exposure_time**(0.75) 65 | 66 | retina_MPE /= exposure_time 67 | 68 | retina_MPE += mask_0700_1050 * mask_above_10s * 1e-3*C_a 69 | retina_MPE += mask_1050_1400 * mask_above_10s * 5e-3*C_c 70 | 71 | cornea_MPE += mask_1200_1400 * mask_10ps_1ms * 0.3*K_l 72 | cornea_MPE += mask_1200_1400 * mask_1ms_4s * 0.3*K_l + 0.56*exposure_time**(0.25) - 0.1 73 | cornea_MPE += mask_1200_1400 * mask_4s_10s * 0.3*K_l + 0.7 74 | 75 | cornea_MPE /= exposure_time # convert to W 76 | 77 | cornea_MPE += mask_1200_1400 * mask_above_10s * 0.03*K_l + 0.07 78 | 79 | cornea_MPE *= 1e4 # cm2 to m2 80 | retina_MPE *= 1e4 81 | 82 | MPE = np.minimum( (retina_MPE == 0)*1e20 + retina_MPE, (cornea_MPE == 0)*1e20 + cornea_MPE) 83 | MPE = (MPE < 1e19)*MPE 84 | 85 | return MPE, retina_MPE, cornea_MPE 86 | 87 | def ocular_MPE_point_source_VIS(wavelength:np.ndarray=1064e-9,exposure_time:np.ndarray=30000): 88 | '''Based on Z136.1 table 5b, return MPE in W.m-2 89 | Valid between 400 and 700 nm''' 90 | all_MPE = 0 91 | 92 | C_b = C_B(wavelength) 93 | t_1 = T_1(wavelength) 94 | 95 | mask_400_450 = (400e-9 <= wavelength)*(wavelength < 450e-9) 96 | mask_450_500 = (450e-9 <= wavelength)*(wavelength < 500e-9) 97 | mask_500_700 = (500e-9 <= wavelength)*(wavelength < 700e-9) 98 | mask_400_500 = mask_400_450 + mask_450_500 99 | mask_400_700 = mask_400_500 + mask_500_700 100 | 101 | mask_100fs_10ps = ( 1e-13 <= exposure_time)*(exposure_time < 1e-11) 102 | mask_10ps_5us = ( 1e-11 <= exposure_time)*(exposure_time < 5e-06) 103 | mask_5us_10s = ( 5e-06 <= exposure_time)*(exposure_time < 10) 104 | mask_10s_100s = ( 10 <= exposure_time)*(exposure_time < 100) 105 | mask_10s_T1 = ( 10 <= exposure_time)*(exposure_time < t_1) 106 | mask_T1_100s = ( t_1 <= exposure_time)*(exposure_time < 100) 107 | mask_above_100s = (exposure_time >= 100) 108 | 109 | all_MPE += mask_400_700 * mask_100fs_10ps * 1e-7 110 | all_MPE += mask_400_700 * mask_10ps_5us * 2e-7 111 | all_MPE += mask_400_700 * mask_5us_10s * 1.8e-3*exposure_time**(0.75) 112 | 113 | all_MPE += mask_400_450 * mask_10s_100s * 1e-2 114 | all_MPE += mask_450_500 * mask_T1_100s * 1e-2*C_b 115 | 116 | all_MPE /= exposure_time # convert to W 117 | 118 | all_MPE += mask_400_500 * mask_above_100s * 1e-4*C_b 119 | all_MPE += mask_450_500 * mask_10s_T1 * 1e-3 120 | all_MPE += mask_500_700 * (mask_10s_100s+mask_above_100s) * 1e-3 121 | 122 | all_MPE *= 1e4 # cm2 to m2 123 | 124 | return all_MPE 125 | 126 | 127 | def limiting_aperture_VIS_NIR(wavelength:np.ndarray=1064e-9,exposure_time:np.ndarray=30000): 128 | mask_0400_1200 = ( 400e-9 <= wavelength)*(wavelength < 1200e-9) 129 | mask_1200_1400 = (1200e-9 <= wavelength)*(wavelength <= 1400e-9) 130 | mask_0400_1400 = mask_0400_1200 + mask_1200_1400 131 | 132 | mask_100fs_300ms = ( 1e-13 <= exposure_time)*(exposure_time < 0.3) 133 | mask_300ms_10s = ( 0.3 <= exposure_time)*(exposure_time < 10) 134 | mask_above_10s = (exposure_time >= 10) 135 | 136 | retina = mask_0400_1400 * 7.0 137 | 138 | cornea = mask_1200_1400 * mask_100fs_300ms * 1.0 139 | cornea += mask_1200_1400 * mask_300ms_10s * 1.5*exposure_time**0.375 # "Under normal conditions these exposure durations would not be used for hazard evaluation or classification." so for what? 140 | cornea += mask_1200_1400 * mask_above_10s * 3.5 141 | 142 | skin = mask_0400_1400 * 3.5 143 | 144 | return retina*1e-3, cornea*1e-3, skin*1e-3 -------------------------------------------------------------------------------- /OLBtools/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright © 2020 Paul Serra, Peter Grenfell, Ondrej cierny 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software. 8 | ''' 9 | 10 | #---------------------------------------------------------- 11 | # Imports 12 | from dataclasses import dataclass 13 | 14 | import numpy as np 15 | import mpmath as mp 16 | import scipy.special as scsp 17 | import scipy.optimize as scop 18 | import scipy.integrate as scin 19 | import scipy.interpolate as scit 20 | 21 | mp.mp.dps = 300 22 | 23 | #---------------------------------------------------------- 24 | # Constants 25 | qe = 1.60217662e-19 # charge of an electron, C (A.s) 26 | kb = 1.38064852e-23 # Boltzmann, J/K 27 | hp = 6.62607004e-34 # Planck, J.s 28 | c = 299.792458e6 # Speed of light, m/s 29 | T = 298.15 # Temperature of electronics, K 30 | Re = 6378e3 # Earth radius, m 31 | mu = 3.986004418e14 # Standard gravitational parameter, m3/s2 32 | we = 7.292115e-5 # Earth rotation, radians/seconds 33 | 34 | earth_rotation = 7.292115e-5 # Earth angular speed, radians by seconds 35 | 36 | min_log = 1e-30 37 | 38 | #---------------------------------------------------------- 39 | # Helpers 40 | def degrees(a): return a*180/np.pi 41 | def radians(a): return a*np.pi/180 42 | 43 | def fwhw_to_radius1e2(fwhm): 44 | return fwhm/np.sqrt(2*np.log(2)) 45 | def fwhw_to_diam1e2(fwhm): 46 | return 2*fwhm/np.sqrt(2*np.log(2)) 47 | def radius1e2_to_fwhw(radius1e2): 48 | return np.sqrt(2*np.log(2))*radius1e2 49 | def diam1e2_to_fwhw(diam1e2): 50 | import warnings 51 | warnings.warn("Was incorrect", UserWarning) 52 | return np.sqrt(2*np.log(2))*diam1e2/2 53 | 54 | def angular_wave_number(vlambda):return 2*np.pi/vlambda 55 | 56 | def extend_integ_axis(arrays,integ_var): 57 | for ar in arrays: 58 | ar = ar[..., np.newaxis] 59 | arl = np.broadcast_arrays(integ_var,h) 60 | return arl[1:] 61 | 62 | mpin = np.frompyfunc(mp.mpf,1,1) 63 | gamma = np.frompyfunc(mp.gamma,1,1) 64 | besselk = np.frompyfunc(mp.besselk,2,1) 65 | hyp1F2 = np.frompyfunc(mp.hyp1f2,4,1) 66 | mpexp = np.frompyfunc(mp.exp,1,1) 67 | mpsin = np.frompyfunc(mp.sin,1,1) 68 | mpout = np.frompyfunc(float,1,1) 69 | 70 | def beam_radius(W_0,G_Theta,G_Lambda): 71 | return W_0/np.sqrt(G_Theta**2 + G_Lambda**2) 72 | 73 | def apply_min(x): 74 | return x #(x>min_log)*x + (x<=min_log)*min_log 75 | 76 | def filter_maximum(wi, wc, n): 77 | # Smooth maximum based on the transfer function of order n butterworth filter 78 | w = wi/wc 79 | ratio = 1 / np.sqrt(1 + w**(2*n)) 80 | if n == 1: return wi*ratio 81 | else: return wi*ratio + wc*(1-ratio) 82 | 83 | #---------------------------------------------------------- 84 | # Link Geometry 85 | 86 | def slant_range(h_0,H,zenith,Re): 87 | '''Slant range for a spacecraft, per appendix A 88 | h_0: Station altitude above atmosphere 0 ref, in m 89 | H: spacecraft altitude above atmosphere 0 ref, in m 90 | Re: distance from geotcenter to atmosphere 0 ref, in m 91 | zenith: ground station zenith angle in radians''' 92 | h_0 = h_0 + Re 93 | H = H + Re 94 | return np.sqrt( (h_0*np.cos(zenith))**2 + H**2 - h_0**2 ) - h_0*np.cos(zenith) 95 | 96 | def earth_centered_intertial_to_latitude_longitude(latitude,longitude,thetaE,x,y,z): 97 | '''Peter Grenfell''' 98 | 99 | ctE = np.cos(thetaE); stE = np.sin(thetaE) 100 | cph = np.cos(latitude); sph = np.sin(latitude) 101 | cla = np.cos(longitude); sla = np.sin(longitude) 102 | 103 | tx = cla*cph*x + sla*cph*y + sph*z 104 | ty = -sla*x + cla*y 105 | tz = -cla*sph*x - sla*sph*y + cph*z 106 | 107 | x = ctE*tx + stE*ty 108 | y = -stE*tx + ctE*ty 109 | z = tz 110 | 111 | return x,y,z 112 | 113 | 114 | def pass_azimuth_elevation_and_time(latitude,longitude,altitude,inclination,min_zenith,max_zenith,npts,mu,Re,we): 115 | '''Peter Grenfell 116 | latitude,longitude 117 | orbit_altitude 118 | min_zenith 119 | Re: distance from geotcenter to atmosphere 0 ref, in m''' 120 | 121 | #Slant range at min zenith (max elevation) 122 | sr_min = slant_range(0,altitude,min_zenith,Re) 123 | 124 | #Slant range at max zenith (min elevation) 125 | sr_max = slant_range(0,altitude,max_zenith,Re) 126 | 127 | def law_of_cosines(a,b,c): 128 | #law of cosines, return angle, c is the far side 129 | return np.arccos( (a**2+b**2-c**2)/(2*a*b) ) 130 | 131 | #Top of pass in earth centered referential, at OGS lat/long 132 | x = altitude*np.sin(min_zenith)*np.cos(inclination) 133 | y = altitude*np.sin(min_zenith)*np.sin(inclination) 134 | h = Re + altitude*np.cos(min_zenith) 135 | 136 | # Angle of OGS from orbit plan at geocenter. 137 | orbit_plan_angle_geocenter = law_of_cosines(Re,Re+altitude,sr_min) 138 | 139 | # Angle of the horizon at geocenter from vertical (zentith = 0), in corbit plane. 140 | horizon_angle_in_plane_geocenter = law_of_cosines(Re,Re+altitude,sr_max) 141 | 142 | # Angle of horizon our of orbit plane, using spherical pythagorean theorem. 143 | horizon_angle_geocenter = np.arccos( np.cos(horizon_angle_in_plane_geocenter) / np.cos(orbit_plan_angle_geocenter) ) 144 | 145 | # Orbit periode 146 | T_orbit = 2*np.pi*np.sqrt( (altitude+Re)**3/mu ) 147 | 148 | # time to the horizon from vertical (zentith = 0) 149 | t_horizon = horizon_angle_geocenter*T_orbit/(2*np.pi) 150 | 151 | # Timescale 152 | t = np.linspace(-t_horizon,t_horizon,npts) 153 | 154 | # Orbit phase 155 | sat_phase = 2*np.pi*t/T_orbit 156 | 157 | #Top of pass in earth centered inertial 158 | #x,y,z = earth_centered_intertial_to_latitude_longitude(-latitude,-longitude,0,x=h,y=x,z=y) 159 | 160 | # Circular equatorial orbit 161 | x_orbit = (Re + altitude)*np.cos(sat_phase) 162 | y_orbit = (Re + altitude)*np.sin(sat_phase) 163 | z_orbit = 0 164 | 165 | # offset from OGS, rotation arround y axis 166 | coff = np.cos(orbit_plan_angle_geocenter) 167 | soff = np.sin(orbit_plan_angle_geocenter) 168 | x_off = coff*x_orbit 169 | y_off = y_orbit 170 | z_off = soff*x_orbit 171 | 172 | # Adding inclnation, rotation arround x axis 173 | cinc = np.cos(inclination) 174 | sinc = np.sin(inclination) 175 | x_inc = x_off 176 | y_inc = cinc*y_off + sinc*z_off 177 | z_inc = -sinc*y_off + cinc*z_off 178 | 179 | # Putting it on top off OGS at t0 180 | x, y, z = earth_centered_intertial_to_latitude_longitude(latitude,longitude,0,x_inc,y_inc,z_inc) 181 | 182 | # OGS coordinates 183 | x0,y0,z0 = earth_centered_intertial_to_latitude_longitude(latitude,longitude,we*t,Re,0,0) 184 | x0,y0,z0 = np.broadcast_arrays(x0,y0,z0) 185 | ogs = np.array([x0,y0,z0]).transpose() 186 | 187 | if 0: 188 | import matplotlib.pyplot as plt 189 | from mpl_toolkits.mplot3d import Axes3D 190 | 191 | fig = plt.figure() 192 | ax = fig.add_subplot(111, projection='3d') 193 | ax.plot(x,y,z) 194 | ax.scatter(x0,y0,z0) 195 | ax.scatter(0,0,0) 196 | 197 | # Sat vector from OGS 198 | xs = x-x0 199 | ys = y-y0 200 | zs = z-z0 201 | ns = np.sqrt(xs**2 + ys**2 + zs**2) 202 | 203 | # satelite unit vector 204 | sat = np.array([xs/ns,ys/ns,zs/ns]).transpose() 205 | 206 | # Zentih angle at OGS 207 | zenith = np.arccos((x0*xs + y0*ys + z0*zs)/ns/Re) 208 | 209 | # Sat unit vector in OGS horizontal plane 210 | sat_horizontal = np.cross(ogs,sat)/Re #90 deg away 211 | 212 | # East vector at OGS 213 | east = np.cross([0,0,1],ogs)/Re 214 | 215 | # North vector at OGS 216 | north = np.cross(ogs,east)/Re 217 | 218 | def dot_2D(a,b): 219 | return a[:,0]*b[:,0] + a[:,1]*b[:,1] + a[:,2]*b[:,2] 220 | 221 | cos_east = dot_2D(sat_horizontal,east) 222 | cos_north = dot_2D(sat_horizontal,north) 223 | 224 | # Azimuth angle at OGS 225 | azimuth = np.arctan2(cos_north,-cos_east) 226 | 227 | #on top of lat/long = 0 228 | #+z 229 | #^ 230 | #| 231 | #O->+y 232 | #+x 233 | 234 | 235 | return t,zenith,azimuth 236 | 237 | #---------------------------------------------------------- 238 | # Link budget functions 239 | #---------------------------------------------------------- 240 | 241 | def path_loss_gaussian(beam_radius, wavelength, distance, rx_diameter, pointing_error=0, M2=1.0): 242 | # From Matlab linkbudget, author O cierny / P serra 243 | '''Calculates path loss given a Gaussian beam, and an Rx aperture at a 244 | specific distance. Assumes normalized input power, returns dB loss. 245 | All units in meters / radians. 246 | beam_radius: 1/e^2 radius of the beam at Tx [m] 247 | pointing_error: misalignment between Tx and Rx [rad] (optional) 248 | M2: M square beam quality factor, unitless (optional)''' 249 | 250 | # Calculate beam waist at the given distance due to diffraction [m] 251 | beam_radius_rx = beam_radius * np.sqrt(1 + ((M2*wavelength*distance)/(np.pi*beam_radius**2))**2) 252 | 253 | # Calculate distance from center of Gaussian due to pointing error [m] 254 | radial_distance = np.tan(pointing_error) * distance 255 | 256 | # Calculate irradiance using Gaussian formula [W/m^2] 257 | irradiance = (2/(np.pi*beam_radius_rx**2))*np.exp((-2*radial_distance**2)/beam_radius_rx**2) 258 | 259 | # Calculate power at Rx aperture [W] 260 | rx_area = np.pi*(rx_diameter/2)**2 # [m^2] 261 | rx_power = irradiance * rx_area # [W] 262 | 263 | # Calculate dB loss 264 | path_loss_dB = 10*np.log10(rx_power) # [dB] 265 | return path_loss_dB 266 | 267 | def gaussian_beam_encircled_ratio(tx_radius, wavelength, distance, rx_radius, M2=1.0): 268 | # Calculate beam waist at the given distance due to diffraction [m] 269 | rx_radius_prop = tx_radius * np.sqrt(1 + ((M2*wavelength*distance)/(np.pi*tx_radius**2))**2) 270 | 271 | # beam cut-off ratio 272 | c = rx_radius / rx_radius_prop 273 | 274 | ratio = (1-np.exp(-2*c**2)) 275 | return ratio 276 | 277 | def fwhm_divergence_to_1e2_beam_radius(fwhm, wavelength): 278 | # From Matlab linkbudget, author O cierny 279 | '''Calculates the 1/e^2 Gaussian beam radius given the full width half 280 | maximum angle. 281 | fwhm: full width half maximum angle [rad] 282 | beam_radius: 1/e^2 beam radius at origin [m]''' 283 | beam_radius = (wavelength * np.sqrt(2*np.log(2))) / (np.pi * fwhm) 284 | return beam_radius 285 | 286 | def fwhm_to_radius(fwhm, wavelength): 287 | import warnings 288 | warnings.warn("deprecated", DeprecationWarning) 289 | return fwhm_divergence_to_1e2_beam_radius(fwhm, wavelength) 290 | 291 | 292 | def half_1e2_divergence_to_1e2_beam_radius(divergence_1e2:np.ndarray, wavelength) -> np.ndarray: 293 | '''Calculates the 1/e^2 Gaussian beam radius given the 1e2 half-angle''' 294 | beam_radius = wavelength / (np.pi * divergence_1e2) 295 | return beam_radius 296 | 297 | def divergence_to_radius(divergence_1e2:np.ndarray, wavelength) -> np.ndarray: 298 | import warnings 299 | warnings.warn("deprecated", DeprecationWarning) 300 | return half_1e2_divergence_to_1e2_beam_radius(divergence_1e2, wavelength) 301 | 302 | #---------------------------------------------------------- 303 | # LOWTRAN functions 304 | #---------------------------------------------------------- 305 | 306 | def LOWTRAN_transmittance(zenith,lambda_min,lambda_max,step=1e7/20,model=5,H=0,haze=0): 307 | '''Based on TransmittanceGround2Space.py example 308 | model: 0-6, see Card1 "model" reference in LOWTRAN user manual. 5=subarctic winter 309 | H: altitude of observer [m] 310 | zenith: observer zenith angle [rad] 311 | lambda_min: shortest wavelength [nm] ??? 312 | lambda_max: longest wavelength cm^-1 ??? 313 | step: wavelength step size 314 | ''' 315 | 316 | import lowtran 317 | 318 | c1 = {'model': model, 319 | 'ihaze': haze, 320 | 'h1': H*1e3, 321 | 'angle': degrees(zenith), 322 | 'wlshort': lambda_min*1e9, 323 | 'wllong': lambda_max*1e9, 324 | 'wlstep': 1e7/step, 325 | } 326 | 327 | TR = lowtran.transmittance(c1) 328 | 329 | return TR 330 | 331 | def transmittance(zenith,wavelength,model,H): 332 | 333 | wavelength = np.asarray(wavelength) 334 | 335 | wl_step_large=1e7/20 336 | #wl_step_min=1e7/5 337 | 338 | lambda_low = 1/(1/wavelength.min() + wl_step_large*1e-2) 339 | lambda_high = 1/(1/wavelength.max() - wl_step_large*1e-2) 340 | 341 | #degrees(zenith) 342 | T = LOWTRAN_transmittance(zenith,lambda_low,lambda_high,wl_step_large,model,H) 343 | 344 | x = T.wavelength_nm[::-1]*1e-9 345 | y = T['transmission'][0,::-1,:] 346 | w = wavelength 347 | 348 | if w.shape: ws = w.shape[0] 349 | else: ws = 1 350 | 351 | r = np.zeros((ws,y.shape[1])) 352 | for n in range(y.shape[1]): 353 | r[:,n] = np.interp(w, x, y[:,n]) 354 | 355 | return r 356 | 357 | #def MODTRAN_transmittance(zenith 358 | 359 | @dataclass 360 | class Quadcell: 361 | '''Class for a 4-quadrant pin detector, provides methods for postion, noise and noise-equivalent angle 362 | gap: size of the gap between quadrants, in m or PSF size units 363 | responsivity: photodiode repsonsivity in A/W 364 | transimpedance: amplifier gain in V/A or ohm 365 | amplifier_noise: output refered noise of the amplifier, in v/rtHz 366 | bandwidth: bandwidth for the detection, in Hz 367 | ''' 368 | 369 | gap:float=0.0 370 | responsivity:float=0.9 371 | transimpedance:float=1e6 372 | amplifier_noise:float=1e-5 373 | bandwidth:float=100.0 374 | 375 | def __post_init__(self): 376 | #Quadrants defined as A,B,C,D, in trigonometric order, in quadcell front view. 377 | # A: +x,+y, B:-x,+y, C:-x,-y, D:+x,-y 378 | 379 | #Cumulative sum of the PSF over each quadrant, as a scipy interpolation function 380 | self.PSF_sum_2D_A = None 381 | self.PSF_sum_2D_B = None 382 | self.PSF_sum_2D_C = None 383 | self.PSF_sum_2D_D = None 384 | 385 | def cumulated_gaussian_PSF_on_square_mask(W,x1,x2,y1,y2): 386 | #W: spot size for the PSF, such as PSF(r) = 2/(pi*W**2)*exp(-2*r**2/w**2) 387 | coef = np.sqrt(2)/W 388 | return 0.25*(scsp.erf(coef*x2)-scsp.erf(coef*x1))*(scsp.erf(coef*y2)-scsp.erf(coef*y1)) 389 | 390 | def cumulated_gaussian_PSF_on_quadrant_mask(W,x1,y1,dx,dy): 391 | #W: spot size for the PSF, such as PSF(r) = 2/(pi*W**2)*exp(-2*r**2/w**2) 392 | coef = np.sqrt(2)/W 393 | if dx == 1: cx = 2*np.exp(-(coef*x1)**2)/np.sqrt(np.pi) 394 | else: cx = 1-scsp.erf(coef*x1) 395 | if dy == 1: cy = 2*np.exp(-(coef*y1)**2)/np.sqrt(np.pi) 396 | else: cy = 1-scsp.erf(coef*y1) 397 | return 0.25*cx*cy 398 | 399 | def set_PSF_gaussian(self,W): 400 | #W: spot size for the PSF, such as PSF(r) = 2/(pi*W**2)*exp(-2*r**2/w**2) 401 | 402 | def culative_sum_function(x,y,dx,dy,grid=False): 403 | assert not grid 404 | x_shifted = self.gap/2-x 405 | y_shifted = self.gap/2-y 406 | return Quadcell.cumulated_gaussian_PSF_on_quadrant_mask(W,x_shifted,y_shifted,dx,dy) 407 | 408 | self.set_all_quadrant_sum(culative_sum_function) 409 | 410 | def set_PSF_1D(self,r_samp,v_samp,n_int2d=1000,deg=3): 411 | ''' Compute and set the cumulative sum of the PSF for each quadrant, using radial PSF samples 412 | The PSF values are linearly interpolated 413 | The PSF is automaticaly normalised 414 | r_samp: sample postion, m or interpolation function input unit 415 | v_samp: sample relative intesisty 416 | n_int2d resolution of the cumulative sum on the quadrant 417 | def: interpolation degree for the cumulative sum on the quadrant 418 | ''' 419 | r1 = r_samp[:-1] 420 | r2 = r_samp[1:] 421 | v1 = v_samp[:-1] 422 | v2 = v_samp[1:] 423 | normalize = np.pi*np.sum(r2**2*v2 + (r1**2+r1*r2+r2**2)*(v1-v2)/3 - r1**2*v1) 424 | v_samp = v_samp/normalize 425 | 426 | rf = r_samp[-1] 427 | x = np.linspace(-rf,rf,n_int2d) 428 | y = np.linspace(-rf,rf,n_int2d) 429 | xx, yy = np.meshgrid(x,y) 430 | r = np.sqrt(xx**2+yy**2) 431 | v = (r<=rf)*np.interp(r,r_samp,v_samp) 432 | 433 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*(2*rf/n_int2d)**2 434 | 435 | v_integ_lookup = scit.RectBivariateSpline(x+self.gap/2,y+self.gap/2,v_integ,kx=deg,ky=deg) 436 | 437 | def lookup_function(x,y,dx,dy,sx,sy,grid=False): 438 | if dx==0 and dy==0: 439 | return np.maximum( v_integ_lookup(x*sx, y*sy, 0,0, grid=False), 0) 440 | elif dx==1: 441 | return sx*v_integ_lookup(x*sx, y*sy, 1,0, grid=False) 442 | elif dy==1: 443 | return sy*v_integ_lookup(x*sx, y*sy, 0,1, grid=False) 444 | 445 | self.PSF_sum_2D_A = lambda x,y,dx=0,dy=0:lookup_function(x,y,dx,dy, 1, 1,grid=False) 446 | self.PSF_sum_2D_B = lambda x,y,dx=0,dy=0:lookup_function(x,y,dx,dy,-1, 1,grid=False) 447 | self.PSF_sum_2D_C = lambda x,y,dx=0,dy=0:lookup_function(x,y,dx,dy,-1,-1,grid=False) 448 | self.PSF_sum_2D_D = lambda x,y,dx=0,dy=0:lookup_function(x,y,dx,dy, 1,-1,grid=False) 449 | 450 | def set_all_quadrant_sum(self,sum_function): 451 | # Set all quadrant if the PSF is axi-sytmetrical. 452 | self.PSF_sum_2D_A = lambda x,y,dx=0,dy=0:sum_function( x, y,dx,dy,grid=False) 453 | self.PSF_sum_2D_B = lambda x,y,dx=0,dy=0:sum_function(-x, y,dx,dy,grid=False) 454 | self.PSF_sum_2D_C = lambda x,y,dx=0,dy=0:sum_function(-x,-y,dx,dy,grid=False) 455 | self.PSF_sum_2D_D = lambda x,y,dx=0,dy=0:sum_function( x,-y,dx,dy,grid=False) 456 | 457 | def set_PSF_2D(self,x_samp,y_samp,v_samp,n_int2d=1000,deg=3): 458 | ''' Compute and set the cumulative sum of the PSF for each quadrant, 2D grid PSF samples 459 | The PSF values are linearly interpolated 460 | The PSF is automaticaly normalised 461 | x_samp: sample postion, m or interpolation function input unit 462 | y_samp: sample postion, m or interpolation function input unit 463 | v_samp: sample relative intesisty 464 | n_int2d resolution of the cumulative sum on the quadrant 465 | deg: interpolation degree for the cumulative sum on the quadrant 466 | ''' 467 | 468 | normalize = np.sum(v_samp)*(x_samp[1]-x_samp[0])*(y_samp[1]-y_samp[0]) 469 | v_samp = v_samp/normalize 470 | 471 | # PSF linear interpolation 472 | x = np.linspace(x_samp[0],x_samp[-1],n_int2d)#[:, np.newaxis] 473 | y = np.linspace(y_samp[0],y_samp[-1],n_int2d)#[np.newaxis, :] 474 | xx, yy = np.meshgrid(x,y) 475 | v_samp_lookup = scit.RectBivariateSpline(x_samp,y_samp,v_samp,kx=1,ky=1) 476 | 477 | dx_dy_integ_coef = (1/n_int2d)**2 478 | dx_dy_integ_coef *= (x_samp[-1]-x_samp[0])*(y_samp[-1]-y_samp[0]) 479 | 480 | #integration over each quadrant, and sum interpolation function, shifted by gap value 481 | v = v_samp_lookup( xx, yy, grid=False) 482 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*dx_dy_integ_coef 483 | self.PSF_sum_2D_A = lambda vx,vy,dx=0,dy=0: (scit.RectBivariateSpline(x+self.gap/2,y+self.gap/2,v_integ,kx=deg,ky=deg))( vx, vy,dx,dy,grid=False) 484 | 485 | v = v_samp_lookup(-xx, yy, grid=False) 486 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*dx_dy_integ_coef 487 | self.PSF_sum_2D_B = lambda vx,vy,dx=0,dy=0: (scit.RectBivariateSpline(x+self.gap/2,y+self.gap/2,v_integ,kx=deg,ky=deg))(-vx, vy,dx,dy,grid=False) 488 | 489 | v = v_samp_lookup(-xx,-yy, grid=False) 490 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*dx_dy_integ_coef 491 | self.PSF_sum_2D_C = lambda vx,vy,dx=0,dy=0: (scit.RectBivariateSpline(x+self.gap/2,y+self.gap/2,v_integ,kx=deg,ky=deg))(-vx,-vy,dx,dy,grid=False) 492 | 493 | v = v_samp_lookup( xx,-yy, grid=False) 494 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*dx_dy_integ_coef 495 | self.PSF_sum_2D_D = lambda vx,vy,dx=0,dy=0: (scit.RectBivariateSpline(x+self.gap/2,y+self.gap/2,v_integ,kx=deg,ky=deg))( vx,-vy,dx,dy,grid=False) 496 | 497 | def eval_quadrants(self,x_spot,y_spot,dx=0,dy=0): 498 | #Normalized quadrant amplitude for a give spot postition 499 | A = self.PSF_sum_2D_A(x_spot,y_spot,dx,dy) 500 | B = self.PSF_sum_2D_B(x_spot,y_spot,dx,dy) 501 | C = self.PSF_sum_2D_C(x_spot,y_spot,dx,dy) 502 | D = self.PSF_sum_2D_D(x_spot,y_spot,dx,dy) 503 | return A,B,C,D 504 | 505 | def response_from_quadrants(self,quadrants): 506 | #give the slope of the quadcell response for given quadrant values 507 | A,B,C,D = quadrants 508 | quad_sum = A+B+C+D 509 | x_resp = ((A+D)-(B+C))/quad_sum 510 | y_resp = ((A+B)-(C+D))/quad_sum 511 | return x_resp, y_resp 512 | 513 | def response(self,x_spot,y_spot): 514 | #give the quadcell response for a give spot position 515 | return self.response_from_quadrants(self.eval_quadrants(x_spot,y_spot)) 516 | 517 | def slope_from_quadrant(self,quadrants,quadrants_da): 518 | #give the slope of the quadcell response for given quadrant values and derivative 519 | A,B,C,D = quadrants 520 | A_da,B_da,C_da,D_da = quadrants_da 521 | quad_sum = A+B+C+D 522 | x_resp_da = (2*(B+C)*(A_da+D_da) - 2*(A+D)*(B_da+C_da))/quad_sum**2 523 | y_resp_da = (2*(D+C)*(A_da+B_da) - 2*(A+B)*(D_da+C_da))/quad_sum**2 524 | 525 | return x_resp_da,y_resp_da 526 | 527 | def slope(self,x_spot,y_spot): 528 | #give the slope of the quadcell response for a given spot position 529 | quad = self.eval_quadrants(x_spot,y_spot,dx=0,dy=0) 530 | quad_dx = self.eval_quadrants(x_spot,y_spot,dx=1,dy=0) 531 | quad_dy = self.eval_quadrants(x_spot,y_spot,dx=0,dy=1) 532 | 533 | x_resp_dx,y_resp_dx = self.slope_from_quadrant(quad,quad_dx) 534 | x_resp_dy,y_resp_dy = self.slope_from_quadrant(quad,quad_dy) 535 | 536 | return x_resp_dx,x_resp_dy,y_resp_dx,y_resp_dy 537 | 538 | def angular_slope(self,x_spot,y_spot,focal_lenght,magnification=1): 539 | x_resp_dx,x_resp_dy,y_resp_dx,y_resp_dy = self.slope(x_spot,y_spot) 540 | 541 | # we need x/arctan(x/y, witch has issue for x=0)\ 542 | # Use series expension arround zero, we don't need a quadcell with large angles: 543 | def x_over_atanxy(x,y): 544 | return y + x**2/(3*y) - 4*x**4/(45*y**3) 545 | 546 | x_angle = x_over_atanxy(x_spot, focal_lenght)*magnification 547 | y_angle = x_over_atanxy(y_spot, focal_lenght)*magnification 548 | 549 | x_resp_dx = x_resp_dx*x_angle 550 | x_resp_dy = x_resp_dy*x_angle 551 | y_resp_dx = y_resp_dx*y_angle 552 | y_resp_dy = y_resp_dy*y_angle 553 | 554 | return x_resp_dx,x_resp_dy,y_resp_dx,y_resp_dy 555 | 556 | def SNR(self,x_spot,y_spot,optical_power,modulation_depth=1.0): 557 | ''' Return the quacel signal SNR 558 | x_spot, y_spot: postion of the spot on the quadcell in m or PSF postition units 559 | optical_power: total spot power, W 560 | 561 | ''' 562 | 563 | A,B,C,D = self.eval_quadrants(x_spot,y_spot,dx=0,dy=0) 564 | all_quadrants = np.stack((A,B,C,D)) 565 | 566 | all_power = all_quadrants*optical_power 567 | all_current = all_power*self.responsivity 568 | all_voltage = all_current*self.transimpedance 569 | all_shot_noise = self.transimpedance*np.sqrt(2*qe*all_current*self.bandwidth) 570 | all_amp_noise = self.amplifier_noise*np.sqrt(self.bandwidth) 571 | all_noise = np.sqrt(all_shot_noise**2+all_amp_noise**2) 572 | 573 | signal = sum([all_voltage[i,:,:]*modulation_depth for i in range(4)]) 574 | noise = np.sqrt(sum([all_noise[i,:,:]**2 for i in range(4)])) 575 | SNR = signal / noise 576 | 577 | return SNR 578 | 579 | def NEA(self,x_spot,y_spot,optical_power,focal_lenght,magnification=1,modulation_depth=1.0): 580 | 581 | dx_dax, dx_day, dy_dax, dy_day = self.angular_slope(x_spot,y_spot,focal_lenght,magnification) 582 | 583 | SNR = self.SNR(x_spot,y_spot,optical_power,modulation_depth) 584 | 585 | NEA = np.sqrt(1/dx_dax**2 + 1/dy_day**2)/SNR 586 | return NEA 587 | 588 | @dataclass 589 | class Photodiode: 590 | '''Class for an APD detector 591 | Uses dark current and excess noise factor to derive noise, and add NEP inquadrature if specified 592 | ''' 593 | 594 | gain:float=1.0 595 | responsivity:float=0.9 596 | bandwidth:float=1e6 597 | excess_noise_factor:float=1.0 598 | dark_current:float=0 599 | amp_noise_density:float=0 600 | 601 | def estimatedNEP(self): 602 | """Find estimated NEP based on shot noise and dark current alone""" 603 | # We want to solve S (signal) for SNR = 1, noise = signal 604 | # S**2 = 2*qe*ENF*(S+dark)*BW + i_amp**2*BW 605 | # S**2 - K*S - K*dark - i_amp**2*BW = 0 with K = 2*qe*ENF*BW 606 | # S = (K + sqrt(K**2 + 4*(K*dark+i_amp**2*BW))) / 2 607 | K = 2*qe*self.excess_noise_factor*self.bandwidth 608 | signal = (K + np.sqrt(K**2 + 4*(K*self.dark_current+self.amp_noise_density**2*self.bandwidth))) / 2 609 | NEP = signal/(self.gain*self.responsivity) / np.sqrt(self.bandwidth) 610 | return NEP 611 | 612 | def signal(self,optical_power): 613 | return self.gain*self.responsivity*optical_power 614 | 615 | def noise(self,optical_power): 616 | APD_shot_noise_squared = 2*qe*self.excess_noise_factor*(self.signal(optical_power)+self.dark_current)*self.bandwidth 617 | amplifier_noise_squared = self.amp_noise_density**2*self.bandwidth 618 | return np.sqrt(APD_shot_noise_squared + amplifier_noise_squared) 619 | 620 | 621 | def SNR(self,optical_power): 622 | #Defined as I/In, In is the noise variance 623 | return self.signal(optical_power)/self.noise(optical_power) 624 | 625 | def supportedBandwidth(self,optical_power,required_SNR): 626 | # Noise RMS is Signal / SNR 627 | supported_noise = self.signal(optical_power)/required_SNR 628 | # Re-arange Photodiode.noise 629 | APD_shot_noise_density_squared = 2*qe*self.excess_noise_factor*(self.signal(optical_power)+self.dark_current) 630 | amplifier_noise_density_squared = self.amp_noise_density**2 631 | supported_bandwidth = supported_noise**2 / (APD_shot_noise_density_squared + amplifier_noise_density_squared) 632 | return supported_bandwidth 633 | 634 | def BER_OOK(SNR): 635 | # With SNR defined as I/In 636 | # signal total amplituide is I, and variance of both 0 and 1 is asumed to In 637 | # Optimal thresholding, at 0 with both normal distribution at +/- I/2 638 | # 0 and 1 CDF(x): 0.5 * [1+ERF( (x +/- I/2)/(sqrt(2)*In) )] 639 | # BER = P(1)*P(0|1) + P(0)*P(1|0) = P(0|1) = 2*CDF(0) 640 | # BER = 0.5*[1+ERF( (-I/2)/(sqrt(2)*In) )] 641 | # BER = 0.5*[1-ERF( I/(In*2*sqrt(2)) )] 642 | # BER = 0.5*[ERFC( (I/In)/(2*sqrt(2)) )] 643 | return 0.5*scsp.erfc(SNR/(2*np.sqrt(2))) 644 | 645 | def SNR_from_BER_OOK(BER): 646 | # With SNR defined as I/In 647 | # Inverse of BER_OOK 648 | # 2*BER = ERFC( (I/In)/(2*sqrt(2)) ) 649 | # ERFCinv(2*BER) = (I/In)/(2*sqrt(2)) 650 | # I/In = ERFCinv(2*BER)*2*sqrt(2) 651 | return scsp.erfcinv(2*BER)*2*np.sqrt(2) 652 | 653 | def BER_OOK_integrated(SNR,PDF): 654 | BER = BER_OOK(SNR) 655 | BER_out_of_pdf = 0.5*( 1- np.sum(PDF, axis = 0) ) 656 | return np.sum(PDF*BER, axis = 0) + BER_out_of_pdf 657 | 658 | def suported_bandwidth_OOK(pd:Photodiode,optical_power,BER): 659 | 660 | #Inverse of BER_OOK: 661 | required_SNR = SNR_from_BER_OOK(BER) 662 | 663 | supported_bandwidth = pd.supportedBandwidth(optical_power,required_SNR) 664 | 665 | return supported_bandwidth 666 | 667 | 668 | # ===================================================================================================== 669 | # Check against other link budget 670 | # ===================================================================================================== 671 | 672 | 673 | 674 | # ===================================================================================================== 675 | # deprecated 676 | # ===================================================================================================== 677 | 678 | def gaussian_PSF_on_square_mask(W,x1,x2,y1,y2): 679 | #W: spot size for the PSF, such as PSF(r) = 2/(pi*W**2)*exp(-2*r**2/w**2) 680 | coef = np.sqrt(2)/W 681 | return 0.25*(scsp.erf(coef*x2)-scsp.erf(coef*x1))*(scsp.erf(coef*y2)-scsp.erf(coef*y1)) 682 | 683 | def sampled_linear_interpolation_PSF_corner(r_samp,v_samp,n_int2d=1000,deg=3): 684 | r1 = r_samp[:-1] 685 | r2 = r_samp[1:] 686 | v1 = v_samp[:-1] 687 | v2 = v_samp[1:] 688 | normalize = np.pi*np.sum(r2**2*v2 + (r1**2+r1*r2+r2**2)*(v1-v2)/3 - r1**2*v1) 689 | v_samp = v_samp/normalize 690 | 691 | rf = r_samp[-1] 692 | x = np.linspace(-rf,rf,n_int2d) 693 | y = np.linspace(-rf,rf,n_int2d) 694 | xx, yy = np.meshgrid(x,y) 695 | r = np.sqrt(xx**2+yy**2) 696 | v = (r<=rf)*np.interp(r,r_samp,v_samp) 697 | 698 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*(2*rf/n_int2d)**2 699 | 700 | v_integ_lookup = scit.RectBivariateSpline(x,y,v_integ,kx=deg,ky=deg) 701 | 702 | return v_integ_lookup 703 | 704 | def sampled_2D_interpolation_PSF_corner(x_samp,y_samp,v_samp,n_int2d=1000,deg=3): 705 | normalize = np.sum(v_samp)*(x_samp[1]-x_samp[0])*(y_samp[1]-y_samp[0]) 706 | v_samp = v_samp/normalize 707 | center_x = np.sum(v_samp*x_samp)/np.sum(v_samp) 708 | center_y = np.sum(v_samp*y_samp)/np.sum(v_samp) 709 | #print(v_samp.shape) 710 | 711 | x = np.linspace(x_samp[0],x_samp[-1],n_int2d) 712 | y = np.linspace(y_samp[0],y_samp[-1],n_int2d) 713 | xx, yy = np.meshgrid(x,y) 714 | v_samp_lookup = scit.RectBivariateSpline(x_samp,y_samp,v_samp,kx=1,ky=1) 715 | v = v_samp_lookup(x,y) 716 | 717 | v_integ = np.cumsum(np.cumsum(v,axis=0),axis=1)*((x_samp[-1]-x_samp[0])/n_int2d)*((y_samp[-1]-y_samp[0])/n_int2d) 718 | 719 | v_integ_lookup = scit.RectBivariateSpline(x,y,v_integ,kx=deg,ky=deg) 720 | 721 | return v_integ_lookup -------------------------------------------------------------------------------- /OLBtools/beams.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import matplotlib.pyplot as plt 4 | import matplotlib.patches as mpt 5 | 6 | #---------------------------------------------------------- 7 | # Beam convertion functions 8 | 9 | def half1e2_to_FWHM(half1e2:np.ndarray) -> np.ndarray: 10 | '''Convert a half angle or radius to a FWHM matching quantity''' 11 | import warnings 12 | warnings.warn("Was incorrect", UserWarning) 13 | FWHM = half1e2*np.sqrt(2*np.log(2)) 14 | return FWHM 15 | 16 | def FWHM_to_half1e2(FWHM:np.ndarray) -> np.ndarray: 17 | '''Convert a FWHM quantity to a matching half angle or radius''' 18 | import warnings 19 | warnings.warn("Was incorrect", UserWarning) 20 | half1e2 = FWHM/np.sqrt(2*np.log(2)) 21 | return half1e2 22 | 23 | def W0_to_radius1e2(W0:np.ndarray) -> np.ndarray: 24 | '''In this module, we assume W0 is the 1/e2 Gaussian beam radius''' 25 | radius1e2 = W0 26 | return radius1e2 27 | 28 | def radius1e2_to_W0(radius1e2:np.ndarray) -> np.ndarray: 29 | '''In this module, we assume W0 is the 1/e2 Gaussian beam radius''' 30 | W0 = radius1e2 31 | return W0 32 | 33 | def W0_to_diam1e2(W0:np.ndarray) -> np.ndarray: 34 | '''Convert W0 to the 1/e2 Gaussian beam diameter''' 35 | diam1e2 = 2*W0 36 | return diam1e2 37 | 38 | def diam1e2_to_W0(diam1e2:np.ndarray) -> np.ndarray: 39 | '''Convert the 1/e2 Gaussian beam diameter to W0''' 40 | W0 = 0.5*diam1e2 41 | return W0 42 | 43 | def W0_to_FWHMdiam(W0:np.ndarray) -> np.ndarray: 44 | '''Convert W0 to the Full Width Half Max Gaussian beam diameter''' 45 | FWHM_diam = half1e2_to_FWHM(W0) 46 | return FWHM_diam 47 | 48 | def FWHMdiam_to_W0(FWHM_diam:np.ndarray) -> np.ndarray: 49 | '''Convert the Full Width Half Max Gaussian beam diameter to W0''' 50 | W0 = FWHM_to_half1e2(FWHM_diam) 51 | return W0 52 | 53 | def W0_to_halfangle1e2(W0:np.ndarray, wavelength) -> np.ndarray: 54 | '''Convert W0 to the 1/e2 Max Gaussian beam half-angle, angles in radians and distances in meters.''' 55 | half_angle_1e2 = wavelength / (np.pi * W0) 56 | return half_angle_1e2 57 | 58 | def halfangle1e2_to_W0(half_angle_1e2:np.ndarray, wavelength) -> np.ndarray: 59 | '''Convert the 1/e2 Max Gaussian beam half-angle to W0, angles in radians and distances in meters.''' 60 | W0 = wavelength / (np.pi * half_angle_1e2) 61 | return W0 62 | 63 | def W0_to_fullangle1e2(W0:np.ndarray, wavelength) -> np.ndarray: 64 | '''Convert W0 to the 1/e2 Max Gaussian beam full angle, angles in radians and distances in meters.''' 65 | full_angle_1e2 = 2*W0_to_halfangle1e2(W0, wavelength) 66 | return full_angle_1e2 67 | 68 | def fullangle1e2_to_W0(full_angle_1e2:np.ndarray, wavelength) -> np.ndarray: 69 | '''Convert the 1/e2 Max Gaussian beam full angle to W0, angles in radians and distances in meters.''' 70 | W0 = halfangle1e2_to_W0(full_angle_1e2/2, wavelength) 71 | return W0 72 | 73 | def W0_to_FWHMangle(W0:np.ndarray, wavelength) -> np.ndarray: 74 | '''Convert W0 to the Full Width Half Max Gaussian beam angle, angles in radians and distances in meters.''' 75 | half_angle_1e2 = W0_to_halfangle1e2(W0, wavelength) 76 | FWHM_angle = half1e2_to_FWHM(half_angle_1e2) 77 | return FWHM_angle 78 | 79 | def FWHMangle_to_W0(FWHM_angle:np.ndarray, wavelength) -> np.ndarray: 80 | '''Convert the Full Width Half Max Gaussian beam angle to W0, angles in radians and distances in meters.''' 81 | half_angle_1e2 = FWHM_to_half1e2(FWHM_angle) 82 | W0 = halfangle1e2_to_W0(half_angle_1e2, wavelength) 83 | return W0 84 | 85 | #---------------------------------------------------------- 86 | # Gaussian beam fits and centroids 87 | 88 | def ravel_I_x_y(I:np.ndarray, x:np.ndarray, y:np.ndarray) -> tuple[np.ndarray,np.ndarray,np.ndarray]: 89 | '''Check if the I/x/y are already 1d, if not generate a mesh for x/y and flattens it''' 90 | 91 | # Check if the data is already 1D 92 | input_shape = I.shape 93 | is_1d = True 94 | if len(input_shape) > 1: 95 | for size in input_shape[1:]: 96 | if size > 1: 97 | is_1d = False 98 | break 99 | 100 | # if not 1d check dimmention, generate x,y arrays, ravel 101 | if not is_1d: 102 | assert I.shape[0] == x.shape[0] 103 | assert I.shape[1] == y.shape[0] 104 | if (I.shape == x.shape) and (I.shape == x.shape): 105 | xx = x 106 | yy = y 107 | else: 108 | xx,yy = np.meshgrid(y,x) 109 | I = I.flatten() 110 | x = xx.flatten() 111 | y = yy.flatten() 112 | 113 | return I,x,y 114 | 115 | def centroid(I:np.ndarray, x:np.ndarray, y:np.ndarray, threshold:float=None) -> tuple[float,float]: 116 | '''Find the centroid of the I array 117 | I: N x M array 118 | x: Nx1 array, cooridnates 119 | y: Mx1 array, cooridnates 120 | threshold: centroid will ignore value below this. Default is 0.3 of I amplitude 121 | return centroid as tuple, for x and y axis, in x and y units''' 122 | 123 | I,x,y = ravel_I_x_y(I,x,y) 124 | 125 | # If no threhold is available 126 | if threshold is None: 127 | min = np.min(I) 128 | max = np.max(I) 129 | threshold = min + 0.3*(max - min) 130 | 131 | mask = I > threshold 132 | weight = mask*I 133 | total = np.sum(weight) 134 | 135 | x_pondarated = weight * x 136 | x_tot = np.sum(x_pondarated) 137 | x_beam_center = x_tot/total 138 | 139 | y_pondarated = weight * y 140 | y_tot = np.sum(y_pondarated) 141 | y_beam_center = y_tot/total 142 | 143 | return x_beam_center,y_beam_center 144 | 145 | def fit_2d_gaussian(I:np.ndarray, x:np.ndarray, y:np.ndarray, threshold:float=None): 146 | '''Fit a 2d Gaussian distribution to I. 147 | I: N x M array, data to fit 148 | x: Nx1 array, cooridnates 149 | y: Mx1 array, cooridnates 150 | Alternatively, shapes can be 1d N for I,x,y 151 | threshold: centroid will ignore value below this. Default is 0.2 of I amplitude 152 | return: 153 | amplitude in I unit, x0, y0 spot position, sigx, sigy, spot size in x/y units 154 | spot principal axis angle in radian''' 155 | 156 | I,x,y = ravel_I_x_y(I,x,y) 157 | 158 | x0_est, y0_est = centroid(I,x,y, threshold=threshold) 159 | 160 | x -= x0_est 161 | y -= y0_est 162 | 163 | # The model can only handle positive values 164 | min = np.min(I) 165 | assert min >= 0 166 | 167 | # If no threhold is available, set to 20% 168 | max = np.max(I) 169 | if threshold is None: 170 | threshold = 0.4*max 171 | 172 | # Remove based on threshold 173 | mask = I > threshold 174 | I = I[mask] 175 | x = x[mask] 176 | y = y[mask] 177 | 178 | if sum(mask) < 10: print(sum(mask)) 179 | 180 | # Normalize 181 | I /= max 182 | angular_scale = np.maximum(np.max(x),np.max(y)) 183 | x /= angular_scale 184 | y /= angular_scale 185 | 186 | # Log domain 187 | logI = -np.log(I) 188 | 189 | # varriance 190 | var = I # Flat noise 191 | 192 | # Find needed power of x,y in a very overcomplicated manner 193 | # Positon of power of x, y 194 | xmask = np.array([2, 0, 1, 1, 0, 0]) 195 | ymask = np.array([0, 2, 1, 0, 1, 0]) 196 | xmask = xmask[np.newaxis,:] + xmask[:,np.newaxis] 197 | ymask = ymask[np.newaxis,:] + ymask[:,np.newaxis] 198 | xx, yy = np.meshgrid(range(6),range(6)) 199 | xx = xx.ravel() 200 | yy = yy.ravel() 201 | 202 | 203 | # Recoord coordinated for each needed power 204 | coord_dict = {} 205 | for xidx,yidx,xpow,ypow in zip(xx.ravel(), yy.ravel(), xmask.ravel(), ymask.ravel()): 206 | power = (xpow,ypow) 207 | coord = (xidx,yidx) 208 | if power in coord_dict: coord_dict[power].append(coord) 209 | else: coord_dict[power] = [coord] 210 | 211 | # Compute the sum of powers 212 | power_dict = {} 213 | for power in coord_dict.keys(): 214 | power_dict[power] = np.sum((x**power[0])*(y**power[1])*var) 215 | 216 | # Place back the power in the LSQ matrix 217 | HtWH = np.zeros((6,6)) 218 | for power, coord_list in coord_dict.items(): 219 | value = power_dict[power] 220 | for coord in coord_list: 221 | HtWH[coord] = value 222 | HtWH[5,5] = np.sum(var) 223 | 224 | HtWY = np.array([ 225 | np.sum(x**2*var*logI), 226 | np.sum(y**2*var*logI), 227 | np.sum(x*y*var*logI), 228 | np.sum(x*var*logI), 229 | np.sum(y*var*logI), 230 | np.sum(1*var*logI), 231 | ])[:,np.newaxis] 232 | 233 | # Solve the matrix inversion 234 | res = np.linalg.solve(HtWH, HtWY) 235 | 236 | px2, py2, pxy, px, py, p1 = res[:,0] 237 | 238 | # Main axis angle 239 | angle = 0.5*np.arctan2(pxy, py2-px2) 240 | 241 | # Spot size 242 | sig_p = px2 + py2 243 | sig_m = np.sqrt( (py2 - px2)**2 + pxy**2 ) 244 | sigy2 = 2 / (sig_p + sig_m) 245 | sigx2 = 2 / (sig_p - sig_m) 246 | 247 | # Sport center 248 | div0 = 1/ (pxy**2 - 4*px2*py2) 249 | y0 = (2*py2*px - pxy*py) * div0 250 | x0 = (2*px2*py - pxy*px) * div0 251 | 252 | # Amplitude 253 | k = np.exp(- ( py2*px**2 - pxy*px*py + px2*py**2 )/( pxy**2 - 2*px2*py2 ) - p1) 254 | 255 | x0 = x0*angular_scale + x0_est 256 | y0 = y0*angular_scale + y0_est 257 | sigx2 *= angular_scale**2 258 | sigy2 *= angular_scale**2 259 | 260 | return k*max, x0, y0, sigx2, sigy2, angle 261 | 262 | def plot_fit_2d_gaussian(ax:plt.Axes, I,x,y, k, x0, y0, sigx2, sigy2, angle, threshold=None): 263 | 264 | # Adapt fit param 265 | sigx = np.sqrt(sigx2) 266 | sigy = np.sqrt(sigy2) 267 | 268 | if threshold: I = I*(I>threshold) 269 | 270 | # Draw the raw data 271 | extent = [x[0],x[-1],y[-1],y[0]] 272 | ax.imshow(I,extent=extent, interpolation='quadric') 273 | 274 | # Cross lines 275 | x_vert = sigx*np.cos(angle) 276 | y_vert = sigx*np.sin(angle) 277 | x_line_vert = [x0 - x_vert, x0 + x_vert] 278 | y_line_vert = [y0 - y_vert, y0 + y_vert] 279 | ax.plot(x_line_vert, y_line_vert, color='r') 280 | x_hori = sigy*np.sin(angle) 281 | y_hori = -sigy*np.cos(angle) 282 | x_line_hori = [x0 - x_hori, x0 + x_hori] 283 | y_line_hori = [y0 - y_hori, y0 + y_hori] 284 | ax.plot(x_line_hori, y_line_hori, color='r') 285 | 286 | # 1/e2 Ellipse 287 | ellipse = mpt.Ellipse(xy=(x0,y0),width=2*sigx,height=2*sigy,angle=angle*180/np.pi,edgecolor='r', fc='None', lw=1) 288 | ax.add_patch(ellipse) -------------------------------------------------------------------------------- /OLBtools/reporting.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | import os 4 | import numpy as np 5 | import OLBtools as olb # this is weird but i'm not gonna update the whole script 6 | from matplotlib.figure import Figure 7 | from matplotlib.axes import Axes 8 | import matplotlib.pyplot as plt 9 | import io 10 | import base64 11 | 12 | import dataclasses 13 | 14 | class Report: 15 | def __init__(self,title:str='Link Budget'): 16 | self.title = title 17 | self.body = '' 18 | 19 | def AddHtml(self, html): 20 | self.body += '

' + html + '

' 21 | 22 | def AddFigure(self, fig:Figure, alt_text:str=''): 23 | ''' Save the current figure as a base64 embeded in html''' 24 | image_string = io.BytesIO() 25 | fig.savefig(image_string, format='jpg') 26 | image_string.seek(0) 27 | image_base64 = base64.b64encode(image_string.read()).decode() 28 | 29 | self.AddHtml('%s' % (image_base64,alt_text)) 30 | 31 | def HtmlString(self): 32 | 33 | report_header = f''' 34 |

{self.title}

35 | ''' 36 | script_file = sys.argv[0] 37 | scrict_name = os.path.basename(script_file) 38 | script_path = os.path.dirname(script_file) 39 | 40 | try: 41 | import git 42 | repo = git.Repo(script_path) 43 | except: 44 | git_string = '' 45 | else: 46 | commit_hash = repo.head.commit.hexsha 47 | changed = [item.a_path for item in repo.index.diff(None)] 48 | if scrict_name in repo.untracked_files: file_status = 'file is not tracked' 49 | elif scrict_name in changed: file_status = 'file changed' 50 | else: file_status = '' 51 | if repo.is_dirty(untracked_files=True): file_status += ', repos is dirty' 52 | git_string = '
Git: %s, %s' % (commit_hash,file_status) 53 | 54 | report_footer = f''' 55 |

56 | File: {scrict_name} {git_string} 57 |
58 | Generated on {str(datetime.datetime.today())}

59 | ''' 60 | 61 | return REPORT_STYLE + report_header + self.body + report_footer 62 | 63 | def AsPDF(self, pdf_file_name='link_budget.pdf'): 64 | html_rep = self.HtmlString() 65 | 66 | import pdfkit 67 | # Todo: find a more flexible option 68 | config = pdfkit.configuration(wkhtmltopdf=r'C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe') 69 | pdfkit.from_string(html_rep, pdf_file_name, configuration=config) 70 | 71 | def AsHtmlPage(self, html_file_name='link_budget.html'): 72 | html_rep = self.HtmlString() 73 | with open(html_file_name,'w') as html_file: html_file.write(html_rep) 74 | 75 | def FormatSI(self, value:float): 76 | value_range = [1e0,1e3,1e6,1e9,1e12] 77 | prefix = ['m', '', 'K', 'M', 'G', 'T'] 78 | 79 | index = 0 80 | while value > value_range[index]: 81 | index+=1 82 | if index == len(value_range): break 83 | value = value / value_range[index-1] 84 | prefix[index] 85 | 86 | return f'{value:.4g} {prefix[index]}' 87 | 88 | 89 | 90 | class Table: 91 | def __init__(self, content:list, colapse_identical_row:bool=False): 92 | self.content = content 93 | self.colapse_identical_row = colapse_identical_row 94 | 95 | def ColapseRow(self,row:list[str]): 96 | if self.colapse_identical_row: 97 | 98 | last = row[0] 99 | row_html = '' 100 | span_counter = 1 101 | 102 | def comit_last(row_html): 103 | if span_counter > 1: #More than one span, use the html attribute 104 | row_html += '%s' % (span_counter, last) 105 | else: row_html += '%s' % (last) 106 | return row_html 107 | 108 | for item in row[1:]: 109 | if item == last: # New item is identical to previous one: 110 | span_counter += 1 # We need the span to be longer 111 | 112 | else: # new item is different, comit the span using last 113 | row_html = comit_last(row_html) 114 | span_counter = 1 # back to single span 115 | last = item # current item is saved for later, next span comit 116 | 117 | # Last item need to be added 118 | row_html = comit_last(row_html) 119 | 120 | else: 121 | row_html = ''.join(['%s' % col for col in row]) 122 | 123 | return row_html 124 | 125 | 126 | def Html(self,html_class='time') -> str: 127 | self.html_rep = '' 128 | for row in self.content: 129 | self.html_rep += '' + self.ColapseRow(row) + '' 130 | self.html_rep = '' % html_class + self.html_rep + '
' 131 | return self.html_rep 132 | 133 | class CapacityVsRange(Report): 134 | 135 | @dataclasses.dataclass 136 | class InputsCase: 137 | range_start : float 138 | range_end : float 139 | optical_power : float 140 | wavelength : float 141 | beam_divergence : float 142 | receive_diameter : float 143 | APD_gain : float 144 | APD_responsivity : float 145 | APD_bandwidth : float 146 | APD_excess_noise : float 147 | APD_dark_current : float 148 | APD_current_noise : float = 0.0 149 | target_BER : float = 1e-5 150 | M2_factor : float = 1.0 151 | pointing_error : float = 0.0 152 | datarate_to_BW : float = 1.0 153 | db_losses : list[float] = dataclasses.field(default_factory=list) 154 | nametag : str = '' 155 | plot_legend : str = '' 156 | 157 | def textList(self) -> list[str]: 158 | return [ 159 | self.nametag, 160 | f'{self.range_start/1000:,.0f} km to {self.range_end/1000:,.0f} km', 161 | f'{self.optical_power*1000:,.0f} mW', 162 | f'{self.wavelength*1e9:.02f} nm', 163 | f'{self.beam_divergence*1e6:.2f} urad 1/e2', 164 | f'{self.receive_diameter*1e3:.2f} mm', 165 | f'{self.APD_gain:.1f}', 166 | f'{self.APD_responsivity:.2f} A/W', 167 | f'{self.APD_bandwidth/1e6:,.0f} MHz', 168 | f'{self.APD_excess_noise:.2f}', 169 | f'{self.APD_dark_current*1e9:.2f} nA', 170 | f'{self.APD_current_noise*1e12:.2f} pA/rtHz', 171 | f'10E{np.log10(self.target_BER):.1f}', 172 | f'{self.pointing_error*1e6:.2f} urad', 173 | f'{self.M2_factor:.2f}', 174 | f'{self.datarate_to_BW:.2f} b/Hz', 175 | ] 176 | 177 | def copy(self) -> 'CapacityVsRange.InputsCase': return dataclasses.replace(self) 178 | 179 | def __init__(self,cases:list[InputsCase] = [], n_points=1000, **karg): 180 | super().__init__(**karg) 181 | self.cases = cases 182 | self.n_points = n_points 183 | 184 | def AddCase(self, case:InputsCase): 185 | self.cases.append(case) 186 | if case.nametag == '': case.nametag = f'Case {len(self.cases)}' 187 | 188 | def MakeCasesTable(self) -> 'Table': 189 | NAME_LIST = [ 190 | 'Case Name', 191 | 'Range', 192 | 'Optical power', 193 | 'Wavelength', 194 | 'Beam divergence', 195 | 'Receiver diameter', 196 | 'APD gain', 197 | 'APD responsivity', 198 | 'APD bandwidth', 199 | 'APD excess noise ratio', 200 | 'APD dark current', 201 | 'APD amplifer noise density', 202 | 'Target uncoded BER', 203 | 'Static pointing error', 204 | 'M2 quality factor', 205 | 'Throughput to Bandwidth ratio', 206 | ] 207 | 208 | param_list = list(zip(NAME_LIST, *[case.textList() for case in self.cases])) 209 | 210 | losses = {} 211 | 212 | for case_index in range(len(self.cases)): 213 | 214 | case = self.cases[case_index] 215 | 216 | for loss_name, loss_value in case.db_losses: 217 | 218 | if not loss_name in losses: losses[loss_name] = [0]*len(self.cases) 219 | 220 | losses[loss_name][case_index] = loss_value 221 | 222 | param_list += [[key]+[f'{v:.1f} dB' for v in vals] for key, vals in losses.items()] 223 | 224 | 225 | return Table(param_list, colapse_identical_row=1) 226 | 227 | def ConcatenateCases(self): 228 | '''Copy over the fileds from the case list, and concatenate them as numpy arrays using the second dimention as case index''' 229 | 230 | for fd in dataclasses.fields(self.InputsCase): 231 | 232 | thing_list = [getattr(case, fd.name) for case in self.cases] 233 | 234 | if fd.name in ('nametag','plot_legend'): # The names are just concatenated ins normal python lists 235 | self.__setattr__(fd.name, thing_list) 236 | elif fd.name == 'db_losses': # Losses needs to be added up first 237 | db_losses = [sum(tuple(zip(*loss_list))[1]) for loss_list in thing_list] 238 | self.db_losses = np.array(db_losses) 239 | else: # it's a value that should be a numpy array, wioth second axis as case index 240 | self.__setattr__(fd.name, np.array(thing_list)) 241 | 242 | #self.range_start = np.array([case.range_start for case in self.cases])[np.newaxis,:] 243 | 244 | def AddParametersTable(self): self.AddHtml('

Parameters

' + self.MakeCasesTable().Html()) 245 | 246 | def Run(self,ranges=None): 247 | 248 | # Generate the inut arrays 249 | self.ConcatenateCases() 250 | 251 | # Log scale link ranges vector 252 | if not ranges: link_range = np.logspace(np.log10(self.range_start),np.log10(self.range_end),self.n_points) 253 | else: link_range = np.array(ranges)[:,np.newaxis] 254 | 255 | # Position error at receiver 256 | r = np.tan(self.pointing_error)*link_range 257 | 258 | # Beam waist 259 | W_0 = olb.divergence_to_radius(self.beam_divergence,self.wavelength) 260 | 261 | # Angular wave number, = 2*pi/lambda 262 | k = olb.angular_wave_number(self.wavelength) 263 | 264 | # Range loss for a gaussian beam 265 | range_loss = olb.path_loss_gaussian(W_0, self.wavelength, link_range, self.receive_diameter, r, self.M2_factor) 266 | 267 | # Addingup all losses 268 | all_losses = range_loss-self.db_losses 269 | 270 | # Received power 271 | P_rx_avg = self.optical_power*10**(all_losses/10) 272 | 273 | # Declare photodetector 274 | pd = olb.Photodiode( 275 | gain=self.APD_gain , 276 | responsivity=self.APD_responsivity, 277 | bandwidth=self.APD_bandwidth, 278 | excess_noise_factor=self.APD_excess_noise, 279 | dark_current=self.APD_dark_current, 280 | amp_noise_density=self.APD_current_noise) 281 | 282 | # Estimate required bandwidth for givien BER 283 | detector_bw = olb.suported_bandwidth_OOK(pd,P_rx_avg,self.target_BER) 284 | 285 | # Supported bandwidth is at most hardware bandwidth 286 | detector_bw = olb.filter_maximum(detector_bw, self.APD_bandwidth, 1) 287 | 288 | # Data is up to 2 time faster than bandwidth 289 | datarate = self.datarate_to_BW*detector_bw 290 | 291 | return link_range, datarate 292 | 293 | def GetThroughputFigure(self) -> tuple[Figure, Axes]: 294 | 295 | link_range, datarate = self.Run() 296 | 297 | fig = plt.figure() 298 | ax = fig.add_subplot(111) 299 | index = 0 300 | for case_name in self.plot_legend: 301 | ax.loglog(link_range[:,index]/1e3, datarate[:,index]/1e6,label=case_name) 302 | index +=1 303 | if index > 1: ax.legend() 304 | ax.set_xlabel('Link range, km') 305 | ax.set_ylabel('Throughput, Mbps') 306 | 307 | return fig, ax 308 | 309 | 310 | def AddSampleTable(self, link_ranges:[int]): 311 | 312 | _, datarate = self.Run(link_ranges) 313 | 314 | sample_list = [] 315 | 316 | sample_list += [['Range'] + [case.nametag for case in self.cases]] 317 | 318 | range_index = 0 319 | for range in link_ranges: 320 | sample_list += [[f'{range*1e-3:,.2f} km'] + [f'{self.FormatSI(dt)}bps' for dt in datarate[range_index,:]]] 321 | range_index += 1 322 | 323 | 324 | self.AddHtml(Table(sample_list).Html()) 325 | 326 | 327 | def GetReport(self): 328 | return self.rep 329 | 330 | REPORT_STYLE = ''' 331 | 368 | ''' 369 | -------------------------------------------------------------------------------- /OLBtools/scintillation.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | 3 | import numpy as np 4 | 5 | import scipy.special as scsp 6 | import scipy.optimize as scop 7 | import scipy.integrate as scin 8 | 9 | #---------------------------------------------------------- 10 | # Cn2 Models 11 | 12 | def Cn2_SLC(h): 13 | # Submarine Laser Comunication (SLC) model, day and night, for h > 1500m 14 | # For some reason, the SLC model is for the atmosphere, and is based on the median values for the AIR Force Maui Optical Station 15 | # Laser Beam Propagation through random Media, 2nd edition Larry C. Andrews and Ronald L. Phillips page 481-482 16 | return (1500<=h)*(h<7200)*8.87e-7/h**3 + (7200<=h)*(h<20000)*2e-16/h**(1/2) 17 | 18 | def Cn2_HV_ACTLIMB(h, H0): 19 | # ACTLIMB (Hufnagel-Valley 5/7 with Gurvich 20 | HV = 1.7e-14*np.exp(-h/100) + 2.7e-16*np.exp(-h/1500) + 3.6e-3*(1e-5*h)**10*np.exp(-h/1000) 21 | Nh2 = 1e-7*np.exp(-2*h/H0) 22 | Ck = 1e-10*10**(h/25e3) 23 | return HV + Nh2*Ck 24 | 25 | def Cn2_HV_57(h): 26 | # Hufnagel-Valley 5/7 27 | HV = 1.7e-14*np.exp(-h/100) + 2.7e-16*np.exp(-h/1500) + 3.6e-3*(1e-5*h)**10*np.exp(-h/1000) 28 | return HV 29 | 30 | def Cn2_HV_ACTLIMB_best(h): 31 | # ACTLIMB best (x0.1) 32 | return 0.1*Cn2_HV_ACTLIMB(h) 33 | 34 | def Cn2_HV_ACTLIMB_worst(h): 35 | # ACTLIMB worst (x10) 36 | return 10*Cn2_HV_ACTLIMB(h) 37 | 38 | def Cn2_HV_best(h): 39 | # HV57 best (x0.1) 40 | return 0.1*Cn2_HV_57(h) 41 | 42 | def Cn2_HV_worst(h): 43 | # HV57 worst (x10) 44 | return 10*Cn2_HV_57(h) 45 | 46 | #---------------------------------------------------------- 47 | 48 | def Rytov_var(Cn2_h,k,L): 49 | # Laser Beam Propagation through random Media, 2nd edition Larry C. Andrews and Ronald L. Phillips page 140 50 | return 1.23*Cn2_h*k**(5/7)*L**(11/6) 51 | 52 | def Fried_param(zenith,k,Cn2,h_0,H,n_int=1000): 53 | '''Fried's pararamter for a ground station. 54 | Andrews Ch12, p492, Eq23 55 | zenith: zenith angle in radians 56 | k: angular wave number 57 | Cn2: function of h 58 | h_0: ground sation altitude 59 | H: target altitude''' 60 | 61 | # Integration range 62 | h = np.linspace(h_0,np.minimum(H,20e3),n_int,axis=-1) 63 | 64 | # Integral term 65 | integ = np.trapz(Cn2(h),h,axis=-1) 66 | 67 | return (0.42/np.cos(zenith)*k**2*integ)**(-3/5) 68 | 69 | def normalized_distance_uplink(h,h_0,H): 70 | '''Normalized diatance, uplink case, for use in Andrews ch12 71 | Andrews Ch12, p490, Eq14''' 72 | return 1-(h-h_0)/(H-h_0) 73 | 74 | def gaussian_beam_parameters_at_waist(L,k,W_0): 75 | G_Lambda_0 = (2*L)/(k*W_0**2) 76 | G_Theta_0 = 1 77 | return G_Lambda_0,G_Theta_0 78 | 79 | def gaussian_beam_parameters_colimated(L,k,W_0): 80 | G_Lambda_0,G_Theta_0 = gaussian_beam_parameters_at_waist(L,k,W_0) 81 | divisor = G_Lambda_0**2 + G_Theta_0**2 82 | G_Lambda = G_Lambda_0/divisor 83 | G_Theta = G_Theta_0/divisor 84 | return G_Lambda,G_Theta 85 | 86 | def mu3u_par(G_Lambda,G_Theta,Cn2,h_0,H,n_int=1000): 87 | G_Lambda,G_Theta,h_0_i,H_i = np.broadcast_arrays(G_Lambda,G_Theta,h_0,H) 88 | 89 | # Integration range 90 | #h = np.linspace(h_0_i,H_i,n_int,axis=-1) 91 | h = np.linspace(h_0,np.minimum(H,20e3),n_int,axis=-1) 92 | 93 | G_Lambda = G_Lambda[..., np.newaxis] 94 | G_Theta = G_Theta[..., np.newaxis] 95 | h_0_i = h_0_i[..., np.newaxis] 96 | H_i = H_i[..., np.newaxis] 97 | 98 | # Integration variable 99 | hnu = normalized_distance_uplink(h,h_0_i,H_i) 100 | 101 | #Eq 55 102 | mu3u_to_integ = Cn2(h)*( 103 | (hnu*(G_Lambda*hnu + 1j*(1-(1-G_Theta)*hnu)))**(5/6) 104 | - G_Lambda**(5/6)*hnu**(5/3) ) 105 | mu3u = np.real(np.trapz(mu3u_to_integ,h,axis=-1)) 106 | 107 | return mu3u 108 | 109 | def mu2d_par(Cn2,h_0,H,n_int=1000): 110 | h_0_i,H_i = np.broadcast_arrays(h_0,H) 111 | 112 | # Integration range 113 | #h = np.linspace(h_0_i,H_i,n_int,axis=-1) 114 | h = np.linspace(h_0,np.minimum(H,20e3),n_int,axis=-1) 115 | 116 | h_0_i = h_0_i[..., np.newaxis] 117 | H_i = H_i[..., np.newaxis] 118 | 119 | # Integration variable 120 | hnu = 1-normalized_distance_uplink(h,h_0_i,H_i) 121 | 122 | #Eq 55 123 | mu2d_to_integ = Cn2(h)*hnu**(5/3) 124 | mu2d = np.real(np.trapz(mu2d_to_integ,h,axis=-1)) 125 | 126 | return mu2d 127 | 128 | def scintillation_weak_uplink_tracked(G_Lambda,G_Theta,Cn2,h_0,H,zenith,k,n_int=1000): 129 | '''Sicntillation under weak fluctuation theory for a tracked uplink 130 | Andreaws ch12, p503, eq55, and p504, eq58. 131 | G_Lambda and G_Theta: Gausian beam parrameters, per Andrews Ch12 p489 eq9 132 | Cn2: function of h 133 | h_0: ground sation altitude 134 | H: target altitude 135 | return normalized scintillation index squared''' 136 | 137 | mu3u = mu3u_par(G_Lambda,G_Theta,Cn2,h_0,H,n_int=1000) 138 | 139 | #Eq 58 140 | sigBu2 = 8.70*mu3u*k**(7/6)*(H-h_0)**(5/6)/np.cos(zenith)**(11/6) 141 | 142 | return sigBu2 143 | 144 | def scintillation_weak_uplink_tracked_alt(W_0,Cn2,h_0,H,zenith,k,n_int=1000): 145 | '''Sicntillation under weak fluctuation theory for a tracked uplink 146 | Andreaws ch12, p503, eq55, and p504, eq58. 147 | G_Lambda and G_Theta: Gausian beam parrameters, per Andrews Ch12 p489 eq9 148 | Cn2: function of h 149 | h_0: ground sation altitude 150 | H: target altitude 151 | return normalized scintillation index squared''' 152 | 153 | W_0,k_i,h_0_i,H_i = np.broadcast_arrays(W_0,k,h_0,H) 154 | 155 | # Integration range 156 | #h = np.linspace(h_0_i,H_i,n_int,axis=-1) 157 | h = np.linspace(h_0,np.minimum(H,20e3),n_int,axis=-1) 158 | 159 | W_0 = W_0[..., np.newaxis] 160 | k_i = k_i[..., np.newaxis] 161 | h_0_i = h_0_i[..., np.newaxis] 162 | H_i = H_i[..., np.newaxis] 163 | 164 | # Integration variable 165 | hnu = normalized_distance_uplink(h,h_0_i,H_i) 166 | 167 | G_Lambda,G_Theta = gaussian_beam_parameters_colimated(h,k_i,W_0) 168 | 169 | #Eq 55 170 | mu3u_to_integ = Cn2(h)*( 171 | (hnu*(G_Lambda*hnu + 1j*(1-(1-G_Theta)*hnu)))**(5/6) 172 | - G_Lambda**(5/6)*hnu**(5/3) ) 173 | mu3u = np.real(np.trapz(mu3u_to_integ,h,axis=-1)) 174 | 175 | #Eq 58 176 | sigBu2 = 8.70*mu3u*k**(7/6)*(H-h_0)**(5/6) 177 | 178 | return sigBu2 179 | 180 | def scintillation_downlink_alt(W_0,Cn2,h_0,H,zenith,k,n_int=1000): 181 | '''Sicntillation under weak fluctuation theory for a tracked uplink 182 | Andreaws ch12, p503, eq55, and p504, eq58. 183 | G_Lambda and G_Theta: Gausian beam parrameters, per Andrews Ch12 p489 eq9 184 | Cn2: function of h 185 | h_0: ground sation altitude 186 | H: target altitude 187 | return normalized scintillation index squared''' 188 | 189 | W_0,k_i,h_0_i,H_i = np.broadcast_arrays(W_0,k,h_0,H) 190 | 191 | # Integration range 192 | #h = np.linspace(h_0_i,H_i,n_int,axis=-1) 193 | h = np.linspace(h_0,np.minimum(H,20e3),n_int,axis=-1) 194 | 195 | h_0_i = h_0_i[..., np.newaxis] 196 | 197 | sigR2int = np.trapz(Cn2(h)*(h-h_0_i)**(5/6),h,axis=-1) 198 | 199 | #Eq 38 200 | sigR2 = 2.25*k**(7/6)*sigR2int/np.cos(zenith)**(11/6) 201 | 202 | return sigR2 203 | 204 | def scintillation_uplink_tracked(G_Theta,sigBu2): 205 | '''Sicntillation without weak theory restrictions for a tracked uplink 206 | Andreaws ch12, p506, eq60. 207 | G_Theta: Gausian beam parrameter, per Andrews Ch12 p489 eq9 208 | sigBu2: normalized scintillation index squared, logitudinal axis, under weak fluctuation theory 209 | return normalized scintillation index squared''' 210 | 211 | sigIltracked2 = np.exp( 212 | 0.49*sigBu2/(1+(1+G_Theta)*0.56*sigBu2**(6/5))**(7/6) 213 | + 0.51*sigBu2/(1+0.69*sigBu2**(6/5))**(5/6) 214 | ) - 1 215 | 216 | return sigIltracked2 217 | 218 | def scintillation_uplink_tracked_xy(G_Theta,sigBu2): 219 | '''Sicntillation without weak theory restrictions for a tracked uplink 220 | Andreaws ch12, p506, eq60. 221 | G_Theta: Gausian beam parrameter, per Andrews Ch12 p489 eq9 222 | sigBu2: normalized scintillation index squared, logitudinal axis, under weak fluctuation theory 223 | return normalized scintillation index, small and large scale, squared''' 224 | 225 | sigIltracked2_x = np.exp(0.49*sigBu2/(1+(1+G_Theta)*0.56*sigBu2**(6/5))**(7/6)) - 1 226 | sigIltracked2_y = np.exp(0.51*sigBu2/(1+0.69*sigBu2**(6/5))**(5/6)) - 1 227 | 228 | return sigIltracked2_x,sigIltracked2_y 229 | 230 | def scintillation_downlink_xy(sigR2): 231 | '''Sicntillation for a downlink 232 | Andreaws ch12, p511, eq68. 233 | sigR2: scintillation index for square plane, squared, per Andrews Ch12 p495 eq38 234 | return normalized scintillation index, small and large scale, squared''' 235 | 236 | sigIltracked2_x = np.exp(0.49*sigR2/(1+1.11*sigR2**(6/5))**(7/6)) - 1 237 | sigIltracked2_y = np.exp(0.51*sigR2/(1+0.69*sigR2**(6/5))**(5/6)) - 1 238 | 239 | return sigIltracked2_x,sigIltracked2_y 240 | 241 | def pointing_error_variance(h_0,H,zenith,W_0,k,r_0,Cr=2*np.pi): 242 | '''Pointing error variance under weak fluctuation theory for uplink 243 | Andreaws ch12, p503, eq53, second approximation. 244 | h_0: ground sation altitude, m 245 | H: target altitude, m 246 | zenith: zenith angle in radians 247 | W_0: 1/e2 beam radius at transmit, m 248 | k: angular wave number 249 | r_0: Fired's parrameter, m 250 | ''' 251 | 252 | a = Cr**2*W_0**2/r_0**2 253 | sigpe2 = 0.54*(H-h_0)**2/np.cos(zenith)**2*(np.pi/(W_0*k))**2*(2*W_0/r_0)**(5/3) \ 254 | *(1-(a/(1+a))**(1/6)) 255 | 256 | return sigpe2 257 | 258 | def pointing_error_variance_alt(h_0,H,zenith,W_0,k,r_0,Cr=2*np.pi,n_int=1000): 259 | '''Pointing error variance under weak fluctuation theory for uplink 260 | Andreaws ch12, p503, eq53, second approximation. 261 | h_0: ground sation altitude, m 262 | H: target altitude, m 263 | zenith: zenith angle in radians 264 | W_0: 1/e2 beam radius at transmit, m 265 | k: angular wave number 266 | r_0: Fired's parrameter, m 267 | ''' 268 | 269 | h_0_i,H_i = np.broadcast_arrays(h_0,H) 270 | 271 | a = Cr**2*W_0**2/r_0**2 272 | 273 | h = np.linspace(h_0_i,np.minimum(H_i,20e3),n_int,axis=-1) 274 | 275 | h_0_i = h_0_i[..., np.newaxis] 276 | H_i = H_i[..., np.newaxis] 277 | 278 | # Integration variable 279 | hnu = normalized_distance_uplink(h,h_0_i,H_i) 280 | 281 | #Eq 55 282 | sigpe2_integ = np.trapz(Cn2(h)*hnu**2,h,axis=-1) 283 | 284 | sigpe2 = 0.725*(H-h_0)**2/np.cos(zenith)**3*W_0**(-1/3) \ 285 | *(1-(a/(1+a))**(1/6))*sigpe2_integ 286 | 287 | return sigpe2 288 | 289 | def scintillation_uplink_untracked(sigpe2,h_0,H,zenith,W_0,r_0,L,r,sigIltracked2,W): 290 | 291 | sigpe = np.sqrt(sigpe2) 292 | 293 | sigIluntracked2 = 5.95*(H-h_0)**2/np.cos(zenith)**2*(2*W_0/r_0)**(5/3) \ 294 | *( ((r-sigpe)/(L*W))**2*((r-sigpe) > 0) + (sigpe/(L*W))**2 ) \ 295 | + sigIltracked2 296 | 297 | return sigIluntracked2 298 | 299 | def get_scintillation_uplink_untracked(h_0,H,zenith,k,W_0,Cn2,r,Cr=2*np.pi): 300 | 301 | L = slant_range(h_0,H,zenith,Re) 302 | 303 | G_Lambda,G_Theta = gaussian_beam_parameters_colimated(L,k,W_0) 304 | 305 | W = beam_radius(W_0,G_Theta,G_Lambda) 306 | 307 | sigBu2 = scintillation_weak_uplink_tracked(G_Lambda,G_Theta,Cn2,h_0,H,zenith,k) 308 | 309 | sigIltracked2 = scintillation_uplink_tracked(G_Theta,sigBu2) 310 | 311 | r_0 = Fried_param(zenith,k,Cn2,h_0,H) 312 | 313 | sigpe2 = pointing_error_variance(h_0,H,zenith,W_0,k,r_0,Cr) 314 | 315 | sigIluntracked2 = scintillation_uplink_untracked(sigpe2,h_0,H,zenith,W_0,r_0,L,r,sigIltracked2,W) 316 | 317 | return sigIluntracked2 318 | 319 | def get_scintillation_uplink_untracked_xy(h_0,H,zenith,k,W_0,Cn2,r,Cr=2*np.pi): 320 | 321 | L = slant_range(h_0,H,zenith,Re) 322 | 323 | G_Lambda,G_Theta = gaussian_beam_parameters_colimated(L,k,W_0) 324 | 325 | W = beam_radius(W_0,G_Theta,G_Lambda) 326 | 327 | sigBu2 = scintillation_weak_uplink_tracked(G_Lambda,G_Theta,Cn2,h_0,H,zenith,k) 328 | 329 | sigIltracked2_x,sigIltracked2_y = scintillation_uplink_tracked_xy(G_Theta,sigBu2) 330 | 331 | r_0 = Fried_param(zenith,k,Cn2,h_0,H) 332 | 333 | sigpe2 = pointing_error_variance(h_0,H,zenith,W_0,k,r_0,Cr) 334 | 335 | sig2_x = scintillation_uplink_untracked(sigpe2,h_0,H,zenith,W_0,r_0,L,r,sigIltracked2_x,W) 336 | 337 | sig2_y = sigIltracked2_y 338 | 339 | sig2_x, sig2_y = np.broadcast_arrays(sig2_x, sig2_y) 340 | 341 | return apply_min(sig2_x), apply_min(sig2_y) 342 | 343 | def get_scintillation_uplink_tracked_xy(h_0,H,zenith,k,W_0,Cn2,Cr=2*np.pi): 344 | 345 | L = slant_range(h_0,H,zenith,Re) 346 | 347 | G_Lambda,G_Theta = gaussian_beam_parameters_colimated(L,k,W_0) 348 | 349 | W = beam_radius(W_0,G_Theta,G_Lambda) 350 | 351 | sigBu2 = scintillation_weak_uplink_tracked(G_Lambda,G_Theta,Cn2,h_0,H,zenith,k) 352 | 353 | sigIltracked2_x,sigIltracked2_y = scintillation_uplink_tracked_xy(G_Theta,sigBu2) 354 | 355 | sig2_x = sigIltracked2_x 356 | 357 | sig2_y = sigIltracked2_y 358 | 359 | return apply_min(sigIltracked2_x), apply_min(sigIltracked2_y) 360 | 361 | def get_scintillation_downlink_xy(h_0,H,zenith,k,W_0,Cn2,Cr=2*np.pi): 362 | 363 | sigR2 = scintillation_downlink_alt(W_0,Cn2,h_0,H,zenith,k) 364 | 365 | sigIltracked2_x,sigIltracked2_y = scintillation_downlink_xy(sigR2) 366 | 367 | sig2_x = sigIltracked2_x 368 | 369 | sig2_y = sigIltracked2_y 370 | 371 | return apply_min(sigIltracked2_x), apply_min(sigIltracked2_y) 372 | 373 | def gamma_gamma_distrib_pdf(sig2_x,sig2_y,Ie,I): 374 | 375 | alpha = 1/mpin(sig2_x) 376 | beta = 1/mpin(sig2_y) 377 | Ie = mpin(Ie) 378 | I = mpin(I) 379 | 380 | avg = (alpha+beta)/2 381 | 382 | P_I = 2*(alpha*beta)**avg/(gamma(alpha)*gamma(beta)*I)*(I/Ie)**avg*besselk(alpha-beta,2*np.sqrt(alpha*beta*I/Ie)) 383 | 384 | return mpout(P_I) 385 | 386 | def gamma_gamma_distrib_cdf_direct(sig2_x,sig2_y,Ie,It): 387 | alpha = 1/mpin(sig2_x) 388 | beta = 1/mpin(sig2_y) 389 | It = mpin(It)/mpin(Ie) 390 | 391 | P_I = np.pi/(mpsin(np.pi*(alpha-beta))*gamma(alpha)*gamma(beta)) \ 392 | *( (alpha*beta*It)**beta /( beta*gamma(beta-alpha+1))*hyp1F2( beta, beta+1,beta-alpha+1,alpha*beta*It) \ 393 | -(alpha*beta*It)**alpha/(alpha*gamma(alpha-beta+1))*hyp1F2(alpha,alpha+1,alpha-beta+1,alpha*beta*It) ) 394 | 395 | return mpout(P_I) 396 | 397 | def gamma_gamma_distrib_cdf_hypercomb(sig2_x,sig2_y,Ie,It): 398 | 399 | pdf = gamma_gamma_distrib_pdf(sig2_x,sig2_y,Ie,I) 400 | 401 | mphypercomb = np.frompyfunc(lambda i,o: mp.hypercomb(i,o,zeroprec=5),2,1) #,verbose=True 402 | array_of_list = np.frompyfunc(lambda u:[u],1,1) 403 | 404 | alpha = 1/mpin(sig2_x) 405 | beta = 1/mpin(sig2_y) 406 | 407 | It = mpin(It)/mpin(Ie) 408 | 409 | def comb_param_function(a,b,I): 410 | tpl1 = ([a*b*I, 1/b], [b, 1], [], [a, b, b-a+1], [b], [b+1, b-a+1], a*b*I) 411 | tpl2 = ([a*b*I, -1/a], [a, 1], [], [a, b, a-b+1], [a], [a+1, a-b+1], a*b*I) 412 | return [tpl1,tpl2] 413 | 414 | hyp_inputs = array_of_list(alpha)+array_of_list(beta)+array_of_list(It) 415 | 416 | P_I = np.pi/(mpsin(np.pi*(alpha-beta))) * mphypercomb(comb_param_function, hyp_inputs) 417 | return mpout(P_I) 418 | 419 | 420 | def gamma_gamma_to_alpha_mu(sig2_x,sig2_y,orders=[2,3],add=False,I0=1): 421 | lgamma = scsp.gammaln 422 | def select_lsq(fun,x0,jac,bounds): return scop.least_squares(fun,x0,jac,bounds,'trf',ftol=1e-15,xtol=1e-15,gtol=1e-15,max_nfev=1000) 423 | array_lsq = np.frompyfunc(select_lsq,4,1) 424 | 425 | bdshape = list(np.broadcast(sig2_x,sig2_y).shape) 426 | 427 | def EXn(n,sigx,sigy,I0): 428 | a = 1/sigx 429 | b = 1/sigy 430 | return scsp.gammaln(a+n) + scsp.gammaln(b+n) - scsp.gammaln(a) - scsp.gammaln(b) - np.log(a*b/I0)*n 431 | 432 | ERgg = [EXn(nx,sig2_x,sig2_y,I0) for nx in orders] 433 | 434 | def ERalphamu(x,ERn,orders): 435 | alpha,mu = x 436 | ERau = [(nx-1)*lgamma(mu)+lgamma(mu+nx/alpha)-nx*lgamma(mu+1/alpha) for nx in orders] 437 | return [ERau[0]-ERn[0],ERau[1]-ERn[1]] 438 | 439 | def Jacobian(x,orders): 440 | alpha,mu = x 441 | digmu = scsp.digamma(mu) 442 | dign = [scsp.digamma(mu+nx/alpha) for nx in orders] 443 | dig1 = scsp.digamma(mu+1/alpha) 444 | return np.array([[ nx/alpha**2*(dig1-dignx), (nx-1)*digmu+dignx-nx*dig1] for nx, dignx in zip(orders,dign)]) 445 | 446 | if add: 447 | func = np.frompyfunc(lambda ER0,ER1: lambda x:ERalphamu(x,[ER0,ER1],orders),2,1)(ERgg[0],ERgg[1]) 448 | initc = np.frompyfunc(lambda alpha0,mu0:[alpha0,mu0],2,1)(0.5,1) 449 | bnds = np.frompyfunc(lambda a0,m0,a1,m1:([a0,m0],[a1,m1]),4,1)(min_log,min_log,np.inf,np.inf) 450 | else: 451 | func = np.frompyfunc(lambda ER0,ER1: lambda x:ERalphamu(x,[ER0,ER1],orders),2,1)(ERgg[0],ERgg[1]) 452 | initc = np.frompyfunc(lambda alpha0,mu0:[alpha0,mu0],2,1)(0.5*np.ones(bdshape),np.sqrt(1/sig2_x/sig2_y)) 453 | bnds = np.frompyfunc(lambda a0,m0,a1,m1:([a0,m0],[a1,m1]),4,1)(min_log*np.ones(bdshape),min_log*np.ones(bdshape),np.inf,np.inf) 454 | jac = lambda x:Jacobian(x,orders) 455 | 456 | res = array_lsq(func,initc,jac,bnds) 457 | resa = np.frompyfunc(lambda obj:tuple(obj.x),1,2)(res) 458 | resst = np.frompyfunc(lambda obj:obj.status,1,1)(res) 459 | #assert np.all(resst != 0) 460 | alpha, mu = resa 461 | mu = mu.astype(np.float64) 462 | alpha = alpha.astype(np.float64) 463 | r = np.exp( np.log(mu)/alpha + lgamma(mu) - lgamma(mu+1/alpha) ) 464 | 465 | return (alpha,mu,r) 466 | 467 | def alpha_mu_cdf(alpha,mu,r,I0,It): 468 | gvar = mu*(It/I0/r)**alpha 469 | gvar,mu = np.broadcast_arrays(gvar,mu) 470 | cdf = 1 - scsp.gammaincc(mu,gvar) 471 | return cdf 472 | 473 | def alpha_mu_inv_cdf(alpha,mu,r,I0,P): 474 | gvar = scsp.gammainccinv(mu,P) 475 | It = I0*r*(gvar/mu)**(1/alpha) 476 | return It 477 | 478 | def alpha_mu_pdf(alpha,mu,r,I0,It): 479 | lgmu = np.log(mu) 480 | lgal = np.log(alpha) 481 | lgP = np.log(It/I0/r) 482 | return 1/I0*np.exp( lgal + mu*lgmu + (alpha*mu)*lgP - scsp.gammaln(mu) -mu*(It/I0/r)**alpha ) 483 | 484 | def alpha_mu_cdf_sum(alpha,mu,r,Pe,internal_scale,output_scale=None,cumulative=False): 485 | 486 | internal_scale_ax = internal_scale[np.newaxis,:] 487 | 488 | if output_scale is None: output_scale = internal_scale 489 | 490 | cdfs = alpha_mu_cdf(alpha[0],mu[0],r[0],Pe[0],internal_scale_ax) 491 | cdfst = cdfs.transpose().flatten() 492 | covoltmp = np.concatenate([[cdfst[0]],cdfst[1:]-cdfst[:-1]],0) 493 | 494 | if cumulative: 495 | covlog = np.interp(output_scale,internal_scale,np.cumsum(covoltmp)) 496 | covolres = np.zeros((len(alpha),len(output_scale))) 497 | covolres[0,:] = covlog 498 | 499 | for i in range(1,len(alpha)): 500 | cdfs = alpha_mu_cdf(alpha[i],mu[i],r[i],Pe[i],internal_scale_ax) 501 | cdfst = cdfs.transpose().flatten() 502 | cdfst = np.concatenate([[cdfst[0]],cdfst[1:]-cdfst[:-1]],0) 503 | 504 | covolval = np.convolve(covoltmp,cdfst,mode='full')[0:(0+len(cdfst))] 505 | covoltmp = covolval 506 | 507 | if cumulative: 508 | covlog = np.interp(output_scale,internal_scale,np.cumsum(covolval)) 509 | covolres[i,:] = covlog 510 | 511 | if cumulative: return covolres 512 | else: return np.interp(output_scale,internal_scale,np.cumsum(covolval)) 513 | 514 | #def SNR0_shot(i_sig):pass 515 | 516 | #def SNR0_APD(i_sig,sig_n): 517 | 518 | def SNR0_NEP(p_sig,NEP,BW): 519 | return p_sig / (NEP*np.sqrt(BW)) 520 | 521 | def SNR0_sig(i_sig,sig_n): return i_sig/sig_n 522 | 523 | def gamma_gamma_BER_NEP_single(sig2_x,sig2_y,Ie,NEP,BW): 524 | def integrand(u): 525 | return gamma_gamma_distrib_pdf(sig2_x,sig2_y,Ie,u)*scsp.erfc(SNR0_NEP(u,NEP,BW)*u/(2*np.sqrt(2))) 526 | try: 527 | return 0.5*scin.quad(integrand,0,np.inf)[0] 528 | except ZeroDivisionError: 529 | return 1 530 | gamma_gamma_BER_NEP = np.frompyfunc(gamma_gamma_BER_NEP_single,5,1) 531 | 532 | 533 | def alpha_mu_BER_NEP_single(alpha,mu,r,Ie,NEP,BW): 534 | def integrand(u): 535 | return alpha_mu_pdf(alpha,mu,r,Ie,u)*scsp.erfc(SNR0_NEP(u,NEP,BW)*u/(2*np.sqrt(2))) 536 | try: 537 | #return 0.5*scin.quad(integrand,0,1e10,points=[Ie])[0] 538 | return 0.5*scin.quad(integrand,0,np.inf)[0] 539 | #return 0.5*np.float(scin.romberg(integrand,0,100)) 540 | except ZeroDivisionError: 541 | return 1 542 | alpha_mu_BER_NEP = np.frompyfunc(alpha_mu_BER_NEP_single,6,1) 543 | 544 | def alpha_mu_BER_NEP_fixed(alpha,mu,r,Ie,NEP,BW): 545 | u = np.linspace(0,100,10000) 546 | integrand = alpha_mu_pdf(alpha,mu,r,Ie,u)*scsp.erfc(SNR0_NEP(u,NEP,BW)*u/(2*np.sqrt(2))) 547 | return 0.5*scin.trapz(integrand,u) 548 | alpha_mu_BER_NEP_f = np.frompyfunc(alpha_mu_BER_NEP_fixed,6,1) 549 | 550 | '''def gamma_gamma_distrib_cdf_alt7(sig2_x,sig2_y,Ie,It,orders=[2,3]): 551 | lgamma = scsp.gammaln 552 | array_fsolve = np.frompyfunc(scop.fsolve,2,3) 553 | def select_lsq(fun,x0,jac,bounds): return scop.least_squares(fun,x0,jac,bounds,'trf',ftol=1e-15,xtol=1e-15,gtol=1e-15,max_nfev=1000) 554 | array_lsq = np.frompyfunc(select_lsq,4,1) 555 | 556 | a = 1/sig2_x 557 | b = 1/sig2_y 558 | It = It/Ie 559 | 560 | bdshape = list(np.broadcast(a,b).shape) 561 | 562 | abdiv = lgamma(a+1)+lgamma(b+1) 563 | abmul = lgamma(a)+lgamma(b) 564 | ERgg = [lgamma(a+nx)+lgamma(b+nx)+(nx-1)*abmul-abdiv for nx in orders] 565 | 566 | def ERalphamu(x,ERn,orders): 567 | alpha,mu = x 568 | 569 | ERau = [(nx-1)*lgamma(mu)+lgamma(mu+nx/alpha)-nx*lgamma(mu+1/alpha) for nx in orders] 570 | 571 | return [ERau[0]-ERn[0],ERau[1]-ERn[1]] 572 | 573 | def Jacobian(x,orders): 574 | alpha,mu = x 575 | digmu = scsp.digamma(mu) 576 | dign = [scsp.digamma(mu+nx/alpha) for nx in orders] 577 | return np.array([[ nx*(nx-1)/alpha**2*dignx, (nx-1)*(digmu-dignx)] for nx, dignx in zip(orders,dign)]) 578 | 579 | func = np.frompyfunc(lambda ER0,ER1: lambda x:ERalphamu(x,[ER0,ER1],orders),2,1)(ERgg[0],ERgg[1]) 580 | initc = np.frompyfunc(lambda alpha0,mu0:[alpha0,mu0],2,1)(0.5*np.ones(bdshape),np.sqrt(a*b)) 581 | min_log = 1e-30*np.ones(bdshape) 582 | bnds = np.frompyfunc(lambda a0,m0,a1,m1:([a0,m0],[a1,m1]),4,1)(min_log,min_log,np.inf,np.inf) 583 | jac = lambda x:Jacobian(x,orders) 584 | 585 | print('solving...') 586 | res = array_lsq(func,initc,jac,bnds) 587 | resa = np.frompyfunc(lambda obj:tuple(obj.x),1,2)(res) 588 | resst = np.frompyfunc(lambda obj:obj.status,1,1)(res) 589 | assert np.all(resst != 0) 590 | alpha, mu = resa 591 | mu = mu.astype(np.float64) 592 | alpha = alpha.astype(np.float64) 593 | r = np.exp( np.log(mu)/alpha + lgamma(mu) - lgamma(mu+1/alpha) ) 594 | print('done!') 595 | 596 | gvar = mu*(It/r)**alpha 597 | gvar,mu = np.broadcast_arrays(gvar,mu) 598 | 599 | cdf = 1 - scsp.gammaincc(mu,gvar) 600 | 601 | return (cdf,alpha,mu,r)''' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optical-Link-Budget 2 | An optical link budget tool, with support for scintillation, beam fit, APDs, and 4 quadrant detectors 3 | 4 | ## installing 5 | 6 | ``` 7 | $ pip install git+ssh://git@github.com/pkage/Optical-Link-Budget.git 8 | ``` 9 | -------------------------------------------------------------------------------- /Z136.1.verification.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | import OLBtools.Z136 as z136 6 | 7 | def C_A_verification(): 8 | plt.figure() 9 | wavelength=np.linspace(400e-9,1400e-9,1000) 10 | plt.semilogy(wavelength, z136.C_A(wavelength)) 11 | plt.xlim(400e-9,1400e-9) 12 | plt.ylim(0.8,12) 13 | 14 | def C_B_verification(): 15 | plt.figure() 16 | wavelength=np.linspace(400e-9,600e-9,1000) 17 | plt.semilogy(wavelength, z136.C_B(wavelength)) 18 | plt.xlim(400e-9,600e-9) 19 | plt.ylim(0.8,1.2e3) 20 | 21 | def C_C_verification(): 22 | plt.figure() 23 | wavelength=np.linspace(1050e-9,1400e-9,1000) 24 | plt.semilogy(wavelength, z136.C_C(wavelength)) 25 | plt.xlim(1050e-9,1400e-9) 26 | plt.ylim(0.8,1.2e6) 27 | 28 | def MPE_verification(): 29 | plt.figure() 30 | exposure_time = np.logspace(-13,1,1000) 31 | _, retina_mpe_850, _ = z136.ocular_MPE_point_source_NIR(850e-9, exposure_time) 32 | mpe_650 = z136.ocular_MPE_point_source_VIS(650e-9, exposure_time) 33 | plt.loglog(exposure_time,retina_mpe_850*exposure_time*1e-4) 34 | plt.loglog(exposure_time,mpe_650*exposure_time*1e-4) 35 | plt.xlim(1e-13,1e1) 36 | plt.ylim(1e-8,1e-1) 37 | 38 | 39 | C_A_verification() 40 | C_B_verification() 41 | C_C_verification() 42 | MPE_verification() 43 | plt.show() -------------------------------------------------------------------------------- /example_1.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import OLBtools as olb 3 | 4 | # Transmit 5 | P_tx = 200e-3 # Transmit power laser, W 6 | lambda_gl = 1550e-9 # Laser 1 wavelenght, m 7 | beam_width = 15e-6 # beam width, FWMH radian 8 | pointing_error = 5e-6 # radian 9 | tx_system_loss = 3 # dB 10 | 11 | # Receive 12 | apperture = 95e-3 # Apperture diameter, m 13 | rx_system_loss = 3 # dB 14 | 15 | link_range = 1000e3 # Link distance, m 16 | 17 | # position error at receiver 18 | r = np.tan(pointing_error)*link_range 19 | 20 | # beam waist 21 | W_0 = olb.fwhm_to_radius(beam_width,lambda_gl) 22 | 23 | # Angular wave number, = 2*pi/lambda 24 | k = olb.angular_wave_number(lambda_gl) 25 | 26 | range_loss = olb.path_loss_gaussian(W_0, lambda_gl, link_range, apperture, pointing_error) 27 | 28 | all_losses = range_loss-tx_system_loss-rx_system_loss 29 | 30 | P_rx = P_tx*10**(all_losses/10) 31 | 32 | print('Received power: %.3f uW' % (P_rx*1e6)) -------------------------------------------------------------------------------- /example_2.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.special as scsp 3 | import matplotlib as mpl 4 | import matplotlib.pyplot as plt 5 | import matplotlib.colors as mpc 6 | 7 | import OLBtools as olb 8 | import OLBtools.scintillation as scint 9 | 10 | # Objectives 11 | elevation_min = olb.radians(00) #00 degrees 12 | elevation_max = olb.radians(90) #90 degrees 13 | 14 | # Orbits 15 | altitude = 500e3 # spacecraft altitude 16 | 17 | # Transmit 18 | P_avg = 0.050 # Transmit power laser, W 19 | lambda_gl = 915e-9 # Laser 1 wavelength, m 20 | beam_width = 1000e-6 # beam width, FWMH radian 21 | 22 | # Receive 23 | aperture = 600e-3 # Aperture diameter, m 24 | aperture_scaling = 0.60 # Fraction of clear aperture 25 | 26 | # Detector 27 | Responsivity = 50 # A.W-1 28 | Fn_apd = 3.2 # Excess noise factor @M=100 29 | i_dark_apd = 1.5e-9 30 | 31 | # Losses 32 | pointing_error = 50e-6 # radian 33 | tx_system_loss = 3.00 # dB (10Log) 34 | rx_system_loss = 3.00 # dB (10Log) 35 | 36 | # Atmosphere 37 | Cn2 = scint.Cn2_HV_57 # Hufnagel-valley 5/7 model 38 | 39 | #---------------------------------------------------------- 40 | 41 | #500 poitnts, from specified minimum elevation to top. 42 | elevation = np.linspace(elevation_min,elevation_max,500) 43 | zenith = np.pi/2-elevation 44 | 45 | H = altitude 46 | h_0 = 0 47 | 48 | # Link range for elevation range 49 | link_range = olb.slant_range(h_0,H,zenith,olb.Re) 50 | 51 | # Misspointing as distance at receiver 52 | r_s = np.tan(pointing_error)*link_range 53 | 54 | # 1/e2 beam radius 55 | W_0 = olb.fwhm_to_radius(beam_width,lambda_gl) 56 | 57 | # Wavenumber 58 | k = olb.angular_wave_number(lambda_gl) 59 | 60 | range_loss = olb.path_loss_gaussian(W_0, lambda_gl, link_range, aperture, pointing_error) 61 | 62 | all_losses = range_loss-tx_system_loss-rx_system_loss 63 | 64 | # Expected value of the power at he receiver 65 | Pe = aperture_scaling*P_avg*10**(all_losses/10) 66 | Pe = Pe[:,np.newaxis] #on second dim 67 | 68 | # Scintillation 69 | sig2_x, sig2_y = scint.get_scintillation_downlink_xy(h_0,H,zenith,k,W_0,Cn2) 70 | sig2_x = sig2_x[:,np.newaxis] 71 | sig2_y = sig2_y[:,np.newaxis] 72 | 73 | # Power distribution coeficients 74 | alpha,mu,r = scint.gamma_gamma_to_alpha_mu(sig2_x,sig2_y,orders=[2,3]) 75 | 76 | # Power log scale, must include 1% to 99% cumulated power 77 | Hv = np.ceil(np.log10(scint.alpha_mu_inv_cdf(alpha,mu,r,Pe,0.01).max())) 78 | Hl = np.floor(np.log10(scint.alpha_mu_inv_cdf(alpha,mu,r,Pe,0.99).min())) 79 | Ps = np.logspace(Hl,Hv,500) 80 | Psn = Ps[np.newaxis,:]#on second dim 81 | # Sacle interval centers 82 | pdfPs = (Ps[1:]+Ps[:-1])/2 83 | dPs = Ps[1:]-Ps[:-1] 84 | dPs = dPs[:,np.newaxis] 85 | 86 | # Cumulative distribution function 87 | cdfs = scint.alpha_mu_cdf(alpha,mu,r,Pe,Psn) 88 | cdfst = cdfs.transpose() 89 | 90 | # Prpablility densisty function, over log scale intervals 91 | pdfst = cdfst[1:,:]-cdfst[:-1,:] 92 | 93 | 94 | pdfPs = pdfPs[:,np.newaxis] 95 | 96 | pd = olb.Photodiode( 97 | gain=1, 98 | responsivity=Responsivity, 99 | bandwidth=10e6, 100 | excess_noise_factor=Fn_apd, 101 | dark_current=i_dark_apd) 102 | SNR_10 = pd.SNR(pdfPs) 103 | BER_10 = olb.BER_OOK_integrated(SNR_10,pdfst) 104 | 105 | pd = olb.Photodiode( 106 | gain=1, 107 | responsivity=Responsivity, 108 | bandwidth=100e6, 109 | excess_noise_factor=Fn_apd, 110 | dark_current=i_dark_apd) 111 | SNR_100 = pd.SNR(pdfPs) 112 | BER_100 = olb.BER_OOK_integrated(SNR_100,pdfst) 113 | 114 | pd = olb.Photodiode( 115 | gain=1, 116 | responsivity=Responsivity, 117 | bandwidth=1000e6, 118 | excess_noise_factor=Fn_apd, 119 | dark_current=i_dark_apd) 120 | SNR_1000 = pd.SNR(pdfPs) 121 | BER_1000 = olb.BER_OOK_integrated(SNR_1000,pdfst) 122 | 123 | if 1: 124 | plt.figure() 125 | plt.yscale('log') 126 | plt.axhline(1e-5,color='k',linestyle='--',label='$BER = 10^{-5}$') 127 | plt.plot(olb.degrees(elevation),BER_10,label='10 MHz detector') 128 | plt.plot(olb.degrees(elevation),BER_100,label='100 MHz detector') 129 | plt.plot(olb.degrees(elevation),BER_1000,label='1 GHz detector') 130 | plt.xlabel('Elevation, degrees') 131 | plt.axvline(30,color='red') 132 | plt.ylabel('BER') 133 | plt.xlim(0,90) 134 | plt.ylim(1e-15,1) 135 | plt.legend() 136 | plt.title('BER, 3dB') 137 | 138 | def plt_1_distrib(sx,sy,psv,pev,pnv): 139 | alpha,mu,r = scint.gamma_gamma_to_alpha_mu(sx,sy,orders=[2,3]) 140 | cdfs = scint.alpha_mu_cdf(alpha,mu,r,pev,pnv) 141 | cdfst = cdfs.transpose() 142 | 143 | p01 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.01) 144 | p10 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.10) 145 | p50 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.50) 146 | p90 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.90) 147 | p99 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.99) 148 | 149 | plt.figure() 150 | plt.yscale('log') 151 | plt.pcolormesh(olb.degrees(elevation),psv, 1-cdfst, cmap = 'RdYlBu') 152 | plt.xlabel('Elevation, degrees') 153 | plt.ylabel('Received power $I_{th}$, W') 154 | #plt.plot(olb.degrees(elevation),pev,color='black',label='Without scintillation') 155 | plt.plot(olb.degrees(elevation),p01,color='black',linestyle='-.',label='1% confidence') 156 | plt.plot(olb.degrees(elevation),p10,color='black',linestyle='--',label='10% confidence') 157 | plt.plot(olb.degrees(elevation),p50,color='black',linestyle=':', label='50% confidence') 158 | plt.plot(olb.degrees(elevation),p90,color='black',linestyle='--',label='90% confidence') 159 | plt.plot(olb.degrees(elevation),p99,color='black',linestyle='-.',label='99% confidence') 160 | plt.ylim([psv[0],psv[-1]]) 161 | plt.clim([0,1]) 162 | plt.legend(loc=4) 163 | plt.colorbar() 164 | 165 | if 1: 166 | plt_1_distrib(sig2_x,sig2_y,Ps,Pe,Psn) 167 | plt.title('$P(I>I_{th})$, 3dB') 168 | plt.ylim([Ps[0],Ps[-1]]) 169 | 170 | plt.show() -------------------------------------------------------------------------------- /example_3.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib as mpl 3 | import matplotlib.pyplot as plt 4 | import matplotlib.colors as mpc 5 | 6 | import OLBtools as olb 7 | import OLBtools.scintillation as scint 8 | 9 | elevation = olb.radians(20) #20 degrees 10 | 11 | # Orbits 12 | altitude = 500e3 # spacecraft altitude 13 | 14 | # Transmit 15 | P_avg = 2.50 # Transmit power laser, W 16 | lambda_gl = 978e-9 # Laser 1 wavelength, m 17 | beam_width_min = 1e-6 # beam width, FWMH radian 18 | beam_width_max = 10e-3 19 | 20 | # Receive 21 | aperture = 95e-3 # aperture diameter, m 22 | 23 | # Losses 24 | pointing_error = 5e-6 # radian 25 | tx_system_loss = 3.00 # dB (10Log) 26 | rx_system_loss = 3.00 # dB (10Log) 27 | 28 | # Atmosphere 29 | Cn2 = scint.Cn2_HV_57 #Hufnagel-valley 5/7 model 30 | 31 | #---------------------------------------------------------- 32 | # LINK 33 | #---------------------------------------------------------- 34 | 35 | zenith = np.pi/2-elevation 36 | 37 | H = altitude 38 | h_0 = 0 39 | 40 | link_range = olb.slant_range(h_0,H,zenith,olb.Re) 41 | r = np.tan(pointing_error)*link_range 42 | beam_width = np.logspace(np.log10(beam_width_min),np.log10(beam_width_max),200) 43 | W_0 = olb.fwhm_to_radius(beam_width,lambda_gl) 44 | k = olb.angular_wave_number(lambda_gl) 45 | 46 | range_loss = olb.path_loss_gaussian(W_0, lambda_gl, link_range, aperture, pointing_error) 47 | all_losses = range_loss-tx_system_loss-rx_system_loss 48 | Pe = P_avg*10**(all_losses/10) 49 | 50 | Ps = np.logspace(-12,-1,200) 51 | Psn = Ps[np.newaxis,:] 52 | Pe = Pe[:,np.newaxis] 53 | 54 | 55 | 56 | sig2_x, sig2_y = scint.get_scintillation_uplink_untracked_xy(h_0,H,zenith,k,W_0,Cn2,r) 57 | sig2_x = sig2_x[:,np.newaxis] 58 | sig2_y = sig2_y[:,np.newaxis] 59 | alpha,mu,r = scint.gamma_gamma_to_alpha_mu(sig2_x,sig2_y,orders=[2,3]) 60 | 61 | def plt_1_distrib(sx,sy,psv,pev,pnv): 62 | alpha,mu,r = scint.gamma_gamma_to_alpha_mu(sx,sy,orders=[2,3]) 63 | cdfs = scint.alpha_mu_cdf(alpha,mu,r,pev,pnv) 64 | cdfst = cdfs.transpose() 65 | 66 | p50 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.50) 67 | p90 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.90) 68 | p99 = scint.alpha_mu_inv_cdf(alpha,mu,r,pev,0.99) 69 | 70 | if 1: 71 | plt.figure() 72 | plt.title('$P(I>I_{th})$, with $C_n^2 = 10 \\times HV_{5/7}$') 73 | plt.yscale('log') 74 | plt.xscale('log') 75 | plt.pcolormesh(beam_width*1e3,psv, 1-cdfst, cmap = 'RdYlBu') 76 | plt.xlabel('Divergence, mrad') 77 | plt.ylabel('Received power $I_{th}$, W') 78 | plt.plot(beam_width*1e3,pev,color='black',label='Without scintillation') 79 | plt.plot(beam_width*1e3,p50,color='black',linestyle=':', label='50% confidence') 80 | plt.plot(beam_width*1e3,p90,color='black',linestyle='--',label='90% confidence') 81 | plt.plot(beam_width*1e3,p99,color='black',linestyle='-.',label='99% confidence') 82 | plt.ylim([psv[0],psv[-1]]) 83 | plt.legend() 84 | plt.colorbar() 85 | 86 | if 1: 87 | plt_1_distrib(sig2_x,sig2_y,Ps,Pe,Psn) 88 | plt.title('$P(I>I_{th})$, uplink, 20 deg elevation') 89 | plt.legend() 90 | 91 | plt.show() -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "contourpy" 5 | version = "1.3.1" 6 | description = "Python library for calculating contours of 2D quadrilateral grids" 7 | optional = false 8 | python-versions = ">=3.10" 9 | files = [ 10 | {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, 11 | {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, 12 | {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, 13 | {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, 14 | {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, 15 | {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, 16 | {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, 17 | {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, 18 | {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, 19 | {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, 20 | {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, 21 | {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, 22 | {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, 23 | {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, 24 | {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, 25 | {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, 26 | {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, 27 | {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, 28 | {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, 29 | {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, 30 | {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, 31 | {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, 32 | {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, 33 | {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, 34 | {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, 35 | {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, 36 | {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, 37 | {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, 38 | {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, 39 | {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, 40 | {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, 41 | {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, 42 | {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, 43 | {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, 44 | {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, 45 | {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, 46 | {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, 47 | {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, 48 | {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, 49 | {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, 50 | {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, 51 | {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, 52 | {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, 53 | {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, 54 | {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, 55 | {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, 56 | {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, 57 | {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, 58 | {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, 59 | {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, 60 | {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, 61 | {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, 62 | {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, 63 | {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, 64 | ] 65 | 66 | [package.dependencies] 67 | numpy = ">=1.23" 68 | 69 | [package.extras] 70 | bokeh = ["bokeh", "selenium"] 71 | docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] 72 | mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] 73 | test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] 74 | test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] 75 | 76 | [[package]] 77 | name = "cycler" 78 | version = "0.12.1" 79 | description = "Composable style cycles" 80 | optional = false 81 | python-versions = ">=3.8" 82 | files = [ 83 | {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, 84 | {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, 85 | ] 86 | 87 | [package.extras] 88 | docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] 89 | tests = ["pytest", "pytest-cov", "pytest-xdist"] 90 | 91 | [[package]] 92 | name = "fonttools" 93 | version = "4.55.3" 94 | description = "Tools to manipulate font files" 95 | optional = false 96 | python-versions = ">=3.8" 97 | files = [ 98 | {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0"}, 99 | {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f"}, 100 | {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841"}, 101 | {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674"}, 102 | {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276"}, 103 | {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5"}, 104 | {file = "fonttools-4.55.3-cp310-cp310-win32.whl", hash = "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261"}, 105 | {file = "fonttools-4.55.3-cp310-cp310-win_amd64.whl", hash = "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5"}, 106 | {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e"}, 107 | {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b"}, 108 | {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90"}, 109 | {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0"}, 110 | {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b"}, 111 | {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765"}, 112 | {file = "fonttools-4.55.3-cp311-cp311-win32.whl", hash = "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f"}, 113 | {file = "fonttools-4.55.3-cp311-cp311-win_amd64.whl", hash = "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72"}, 114 | {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35"}, 115 | {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c"}, 116 | {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7"}, 117 | {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314"}, 118 | {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427"}, 119 | {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a"}, 120 | {file = "fonttools-4.55.3-cp312-cp312-win32.whl", hash = "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07"}, 121 | {file = "fonttools-4.55.3-cp312-cp312-win_amd64.whl", hash = "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54"}, 122 | {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29"}, 123 | {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4"}, 124 | {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca"}, 125 | {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b"}, 126 | {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048"}, 127 | {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe"}, 128 | {file = "fonttools-4.55.3-cp313-cp313-win32.whl", hash = "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628"}, 129 | {file = "fonttools-4.55.3-cp313-cp313-win_amd64.whl", hash = "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b"}, 130 | {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3"}, 131 | {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d"}, 132 | {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa"}, 133 | {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e"}, 134 | {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de"}, 135 | {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926"}, 136 | {file = "fonttools-4.55.3-cp38-cp38-win32.whl", hash = "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b"}, 137 | {file = "fonttools-4.55.3-cp38-cp38-win_amd64.whl", hash = "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56"}, 138 | {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af"}, 139 | {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831"}, 140 | {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02"}, 141 | {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4"}, 142 | {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd"}, 143 | {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"}, 144 | {file = "fonttools-4.55.3-cp39-cp39-win32.whl", hash = "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851"}, 145 | {file = "fonttools-4.55.3-cp39-cp39-win_amd64.whl", hash = "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d"}, 146 | {file = "fonttools-4.55.3-py3-none-any.whl", hash = "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977"}, 147 | {file = "fonttools-4.55.3.tar.gz", hash = "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45"}, 148 | ] 149 | 150 | [package.extras] 151 | all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] 152 | graphite = ["lz4 (>=1.7.4.2)"] 153 | interpolatable = ["munkres", "pycairo", "scipy"] 154 | lxml = ["lxml (>=4.0)"] 155 | pathops = ["skia-pathops (>=0.5.0)"] 156 | plot = ["matplotlib"] 157 | repacker = ["uharfbuzz (>=0.23.0)"] 158 | symfont = ["sympy"] 159 | type1 = ["xattr"] 160 | ufo = ["fs (>=2.2.0,<3)"] 161 | unicode = ["unicodedata2 (>=15.1.0)"] 162 | woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] 163 | 164 | [[package]] 165 | name = "gitdb" 166 | version = "4.0.11" 167 | description = "Git Object Database" 168 | optional = false 169 | python-versions = ">=3.7" 170 | files = [ 171 | {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, 172 | {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, 173 | ] 174 | 175 | [package.dependencies] 176 | smmap = ">=3.0.1,<6" 177 | 178 | [[package]] 179 | name = "gitpython" 180 | version = "3.1.43" 181 | description = "GitPython is a Python library used to interact with Git repositories" 182 | optional = false 183 | python-versions = ">=3.7" 184 | files = [ 185 | {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, 186 | {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, 187 | ] 188 | 189 | [package.dependencies] 190 | gitdb = ">=4.0.1,<5" 191 | 192 | [package.extras] 193 | doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] 194 | test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] 195 | 196 | [[package]] 197 | name = "kiwisolver" 198 | version = "1.4.7" 199 | description = "A fast implementation of the Cassowary constraint solver" 200 | optional = false 201 | python-versions = ">=3.8" 202 | files = [ 203 | {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, 204 | {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, 205 | {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, 206 | {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, 207 | {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, 208 | {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, 209 | {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, 210 | {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, 211 | {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, 212 | {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, 213 | {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, 214 | {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, 215 | {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, 216 | {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, 217 | {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, 218 | {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, 219 | {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, 220 | {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, 221 | {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, 222 | {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, 223 | {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, 224 | {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, 225 | {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, 226 | {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, 227 | {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, 228 | {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, 229 | {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, 230 | {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, 231 | {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, 232 | {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, 233 | {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, 234 | {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, 235 | {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, 236 | {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, 237 | {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, 238 | {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, 239 | {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, 240 | {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, 241 | {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, 242 | {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, 243 | {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, 244 | {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, 245 | {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, 246 | {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, 247 | {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, 248 | {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, 249 | {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, 250 | {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, 251 | {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, 252 | {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, 253 | {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, 254 | {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, 255 | {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, 256 | {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, 257 | {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, 258 | {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, 259 | {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, 260 | {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, 261 | {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, 262 | {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, 263 | {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, 264 | {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, 265 | {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, 266 | {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, 267 | {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, 268 | {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, 269 | {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, 270 | {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, 271 | {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, 272 | {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, 273 | {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, 274 | {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, 275 | {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, 276 | {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, 277 | {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, 278 | {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, 279 | {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, 280 | {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, 281 | {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, 282 | {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, 283 | {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, 284 | {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, 285 | {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, 286 | {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, 287 | {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, 288 | {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, 289 | {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, 290 | {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, 291 | {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, 292 | {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, 293 | {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, 294 | {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, 295 | {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, 296 | {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, 297 | {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, 298 | {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, 299 | {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, 300 | {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, 301 | {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, 302 | {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, 303 | {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, 304 | {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, 305 | {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, 306 | {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, 307 | {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, 308 | {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, 309 | {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, 310 | {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, 311 | {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, 312 | {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, 313 | {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, 314 | {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, 315 | {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, 316 | {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, 317 | ] 318 | 319 | [[package]] 320 | name = "lowtran" 321 | version = "3.1.0" 322 | description = "Model of Earth atmosphere absorption and transmission vs. wavelength and location." 323 | optional = false 324 | python-versions = ">=3.8" 325 | files = [ 326 | {file = "lowtran-3.1.0-py3-none-any.whl", hash = "sha256:e9efd6208a074fac488c71b04775ce6964079c2846e6ce5b7c4d6e728c708fca"}, 327 | {file = "lowtran-3.1.0.tar.gz", hash = "sha256:51cfc2d882423e32933ef6be765d7aad97c3432300247d04164a0f76613579f5"}, 328 | ] 329 | 330 | [package.dependencies] 331 | numpy = "*" 332 | python-dateutil = "*" 333 | xarray = "*" 334 | 335 | [package.extras] 336 | lint = ["flake8", "flake8-blind-except", "flake8-bugbear", "flake8-builtins", "mypy", "types-python-dateutil"] 337 | tests = ["pytest"] 338 | 339 | [[package]] 340 | name = "matplotlib" 341 | version = "3.9.3" 342 | description = "Python plotting package" 343 | optional = false 344 | python-versions = ">=3.9" 345 | files = [ 346 | {file = "matplotlib-3.9.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:41b016e3be4e740b66c79a031a0a6e145728dbc248142e751e8dab4f3188ca1d"}, 347 | {file = "matplotlib-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e0143975fc2a6d7136c97e19c637321288371e8f09cff2564ecd73e865ea0b9"}, 348 | {file = "matplotlib-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f459c8ee2c086455744723628264e43c884be0c7d7b45d84b8cd981310b4815"}, 349 | {file = "matplotlib-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687df7ceff57b8f070d02b4db66f75566370e7ae182a0782b6d3d21b0d6917dc"}, 350 | {file = "matplotlib-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:edd14cf733fdc4f6e6fe3f705af97676a7e52859bf0044aa2c84e55be739241c"}, 351 | {file = "matplotlib-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c40c244221a1adbb1256692b1133c6fb89418df27bf759a31a333e7912a4010"}, 352 | {file = "matplotlib-3.9.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cf2a60daf6cecff6828bc608df00dbc794380e7234d2411c0ec612811f01969d"}, 353 | {file = "matplotlib-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:213d6dc25ce686516208d8a3e91120c6a4fdae4a3e06b8505ced5b716b50cc04"}, 354 | {file = "matplotlib-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c52f48eb75fcc119a4fdb68ba83eb5f71656999420375df7c94cc68e0e14686e"}, 355 | {file = "matplotlib-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c93796b44fa111049b88a24105e947f03c01966b5c0cc782e2ee3887b790a3"}, 356 | {file = "matplotlib-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd1077b9a09b16d8c3c7075a8add5ffbfe6a69156a57e290c800ed4d435bef1d"}, 357 | {file = "matplotlib-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:c96eeeb8c68b662c7747f91a385688d4b449687d29b691eff7068a4602fe6dc4"}, 358 | {file = "matplotlib-3.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a361bd5583bf0bcc08841df3c10269617ee2a36b99ac39d455a767da908bbbc"}, 359 | {file = "matplotlib-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e14485bb1b83eeb3d55b6878f9560240981e7bbc7a8d4e1e8c38b9bd6ec8d2de"}, 360 | {file = "matplotlib-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8d279f78844aad213c4935c18f8292a9432d51af2d88bca99072c903948045"}, 361 | {file = "matplotlib-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6c12514329ac0d03128cf1dcceb335f4fbf7c11da98bca68dca8dcb983153a9"}, 362 | {file = "matplotlib-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6e9de2b390d253a508dd497e9b5579f3a851f208763ed67fdca5dc0c3ea6849c"}, 363 | {file = "matplotlib-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d796272408f8567ff7eaa00eb2856b3a00524490e47ad505b0b4ca6bb8a7411f"}, 364 | {file = "matplotlib-3.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:203d18df84f5288973b2d56de63d4678cc748250026ca9e1ad8f8a0fd8a75d83"}, 365 | {file = "matplotlib-3.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b651b0d3642991259109dc0351fc33ad44c624801367bb8307be9bfc35e427ad"}, 366 | {file = "matplotlib-3.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66d7b171fecf96940ce069923a08ba3df33ef542de82c2ff4fe8caa8346fa95a"}, 367 | {file = "matplotlib-3.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be0ba61f6ff2e6b68e4270fb63b6813c9e7dec3d15fc3a93f47480444fd72f0"}, 368 | {file = "matplotlib-3.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d6b2e8856dec3a6db1ae51aec85c82223e834b228c1d3228aede87eee2b34f9"}, 369 | {file = "matplotlib-3.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:90a85a004fefed9e583597478420bf904bb1a065b0b0ee5b9d8d31b04b0f3f70"}, 370 | {file = "matplotlib-3.9.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3119b2f16de7f7b9212ba76d8fe6a0e9f90b27a1e04683cd89833a991682f639"}, 371 | {file = "matplotlib-3.9.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:87ad73763d93add1b6c1f9fcd33af662fd62ed70e620c52fcb79f3ac427cf3a6"}, 372 | {file = "matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:026bdf3137ab6022c866efa4813b6bbeddc2ed4c9e7e02f0e323a7bca380dfa0"}, 373 | {file = "matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760a5e89ebbb172989e8273024a1024b0f084510b9105261b3b00c15e9c9f006"}, 374 | {file = "matplotlib-3.9.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a42b9dc42de2cfe357efa27d9c50c7833fc5ab9b2eb7252ccd5d5f836a84e1e4"}, 375 | {file = "matplotlib-3.9.3-cp313-cp313t-win_amd64.whl", hash = "sha256:e0fcb7da73fbf67b5f4bdaa57d85bb585a4e913d4a10f3e15b32baea56a67f0a"}, 376 | {file = "matplotlib-3.9.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:031b7f5b8e595cc07def77ec5b58464e9bb67dc5760be5d6f26d9da24892481d"}, 377 | {file = "matplotlib-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fa6e193c14d6944e0685cdb527cb6b38b0e4a518043e7212f214113af7391da"}, 378 | {file = "matplotlib-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6eefae6effa0c35bbbc18c25ee6e0b1da44d2359c3cd526eb0c9e703cf055d"}, 379 | {file = "matplotlib-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d3e5c7a99bd28afb957e1ae661323b0800d75b419f24d041ed1cc5d844a764"}, 380 | {file = "matplotlib-3.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:816a966d5d376bf24c92af8f379e78e67278833e4c7cbc9fa41872eec629a060"}, 381 | {file = "matplotlib-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fb0b37c896172899a4a93d9442ffdc6f870165f59e05ce2e07c6fded1c15749"}, 382 | {file = "matplotlib-3.9.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f2a4ea08e6876206d511365b0bc234edc813d90b930be72c3011bbd7898796f"}, 383 | {file = "matplotlib-3.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b081dac96ab19c54fd8558fac17c9d2c9cb5cc4656e7ed3261ddc927ba3e2c5"}, 384 | {file = "matplotlib-3.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a0a63cb8404d1d1f94968ef35738900038137dab8af836b6c21bb6f03d75465"}, 385 | {file = "matplotlib-3.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:896774766fd6be4571a43bc2fcbcb1dcca0807e53cab4a5bf88c4aa861a08e12"}, 386 | {file = "matplotlib-3.9.3.tar.gz", hash = "sha256:cd5dbbc8e25cad5f706845c4d100e2c8b34691b412b93717ce38d8ae803bcfa5"}, 387 | ] 388 | 389 | [package.dependencies] 390 | contourpy = ">=1.0.1" 391 | cycler = ">=0.10" 392 | fonttools = ">=4.22.0" 393 | kiwisolver = ">=1.3.1" 394 | numpy = ">=1.23" 395 | packaging = ">=20.0" 396 | pillow = ">=8" 397 | pyparsing = ">=2.3.1" 398 | python-dateutil = ">=2.7" 399 | 400 | [package.extras] 401 | dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] 402 | 403 | [[package]] 404 | name = "mpmath" 405 | version = "1.3.0" 406 | description = "Python library for arbitrary-precision floating-point arithmetic" 407 | optional = false 408 | python-versions = "*" 409 | files = [ 410 | {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, 411 | {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, 412 | ] 413 | 414 | [package.extras] 415 | develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] 416 | docs = ["sphinx"] 417 | gmpy = ["gmpy2 (>=2.1.0a4)"] 418 | tests = ["pytest (>=4.6)"] 419 | 420 | [[package]] 421 | name = "numpy" 422 | version = "2.2.0" 423 | description = "Fundamental package for array computing in Python" 424 | optional = false 425 | python-versions = ">=3.10" 426 | files = [ 427 | {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"}, 428 | {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"}, 429 | {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"}, 430 | {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"}, 431 | {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"}, 432 | {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"}, 433 | {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"}, 434 | {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"}, 435 | {file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"}, 436 | {file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"}, 437 | {file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"}, 438 | {file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"}, 439 | {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"}, 440 | {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"}, 441 | {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"}, 442 | {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"}, 443 | {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"}, 444 | {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"}, 445 | {file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"}, 446 | {file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"}, 447 | {file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"}, 448 | {file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"}, 449 | {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"}, 450 | {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"}, 451 | {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"}, 452 | {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"}, 453 | {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"}, 454 | {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"}, 455 | {file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"}, 456 | {file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"}, 457 | {file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"}, 458 | {file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"}, 459 | {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"}, 460 | {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"}, 461 | {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"}, 462 | {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"}, 463 | {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"}, 464 | {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"}, 465 | {file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"}, 466 | {file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"}, 467 | {file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"}, 468 | {file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"}, 469 | {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"}, 470 | {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"}, 471 | {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"}, 472 | {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"}, 473 | {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"}, 474 | {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"}, 475 | {file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"}, 476 | {file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"}, 477 | {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"}, 478 | {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"}, 479 | {file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"}, 480 | {file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"}, 481 | {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, 482 | ] 483 | 484 | [[package]] 485 | name = "packaging" 486 | version = "24.2" 487 | description = "Core utilities for Python packages" 488 | optional = false 489 | python-versions = ">=3.8" 490 | files = [ 491 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 492 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 493 | ] 494 | 495 | [[package]] 496 | name = "pandas" 497 | version = "2.2.3" 498 | description = "Powerful data structures for data analysis, time series, and statistics" 499 | optional = false 500 | python-versions = ">=3.9" 501 | files = [ 502 | {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, 503 | {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, 504 | {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, 505 | {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, 506 | {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, 507 | {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, 508 | {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, 509 | {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, 510 | {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, 511 | {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, 512 | {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, 513 | {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, 514 | {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, 515 | {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, 516 | {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, 517 | {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, 518 | {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, 519 | {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, 520 | {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, 521 | {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, 522 | {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, 523 | {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, 524 | {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, 525 | {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, 526 | {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, 527 | {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, 528 | {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, 529 | {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, 530 | {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, 531 | {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, 532 | {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, 533 | {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, 534 | {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, 535 | {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, 536 | {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, 537 | {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, 538 | {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, 539 | {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, 540 | {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, 541 | {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, 542 | {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, 543 | {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, 544 | ] 545 | 546 | [package.dependencies] 547 | numpy = [ 548 | {version = ">=1.22.4", markers = "python_version < \"3.11\""}, 549 | {version = ">=1.23.2", markers = "python_version == \"3.11\""}, 550 | {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, 551 | ] 552 | python-dateutil = ">=2.8.2" 553 | pytz = ">=2020.1" 554 | tzdata = ">=2022.7" 555 | 556 | [package.extras] 557 | all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] 558 | aws = ["s3fs (>=2022.11.0)"] 559 | clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] 560 | compression = ["zstandard (>=0.19.0)"] 561 | computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] 562 | consortium-standard = ["dataframe-api-compat (>=0.1.7)"] 563 | excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] 564 | feather = ["pyarrow (>=10.0.1)"] 565 | fss = ["fsspec (>=2022.11.0)"] 566 | gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] 567 | hdf5 = ["tables (>=3.8.0)"] 568 | html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] 569 | mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] 570 | output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] 571 | parquet = ["pyarrow (>=10.0.1)"] 572 | performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] 573 | plot = ["matplotlib (>=3.6.3)"] 574 | postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] 575 | pyarrow = ["pyarrow (>=10.0.1)"] 576 | spss = ["pyreadstat (>=1.2.0)"] 577 | sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] 578 | test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] 579 | xml = ["lxml (>=4.9.2)"] 580 | 581 | [[package]] 582 | name = "pdfkit" 583 | version = "1.0.0" 584 | description = "Wkhtmltopdf python wrapper to convert html to pdf using the webkit rendering engine and qt" 585 | optional = false 586 | python-versions = "*" 587 | files = [ 588 | {file = "pdfkit-1.0.0-py2-none-any.whl", hash = "sha256:cc122e5aed594198ff7aaa566f2950d2163763576ab891c161bb1f6c630f5a8e"}, 589 | {file = "pdfkit-1.0.0-py3-none-any.whl", hash = "sha256:a7a4ca0d978e44fa8310c4909f087052430a6e8e0b1dd7ceef657f139789f96f"}, 590 | {file = "pdfkit-1.0.0.tar.gz", hash = "sha256:992f821e1e18fc8a0e701ecae24b51a2d598296a180caee0a24c0af181da02a9"}, 591 | ] 592 | 593 | [[package]] 594 | name = "pillow" 595 | version = "11.0.0" 596 | description = "Python Imaging Library (Fork)" 597 | optional = false 598 | python-versions = ">=3.9" 599 | files = [ 600 | {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, 601 | {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, 602 | {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, 603 | {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, 604 | {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, 605 | {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, 606 | {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, 607 | {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, 608 | {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, 609 | {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, 610 | {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, 611 | {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, 612 | {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, 613 | {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, 614 | {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, 615 | {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, 616 | {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, 617 | {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, 618 | {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, 619 | {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, 620 | {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, 621 | {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, 622 | {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, 623 | {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, 624 | {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, 625 | {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, 626 | {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, 627 | {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, 628 | {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, 629 | {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, 630 | {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, 631 | {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, 632 | {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, 633 | {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, 634 | {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, 635 | {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, 636 | {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, 637 | {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, 638 | {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, 639 | {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, 640 | {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, 641 | {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, 642 | {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, 643 | {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, 644 | {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, 645 | {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, 646 | {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, 647 | {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, 648 | {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, 649 | {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, 650 | {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, 651 | {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, 652 | {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, 653 | {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, 654 | {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, 655 | {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, 656 | {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, 657 | {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, 658 | {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, 659 | {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, 660 | {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, 661 | {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, 662 | {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, 663 | {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, 664 | {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, 665 | {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, 666 | {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, 667 | {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, 668 | {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, 669 | {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, 670 | {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, 671 | {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, 672 | {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, 673 | {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, 674 | {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, 675 | ] 676 | 677 | [package.extras] 678 | docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] 679 | fpx = ["olefile"] 680 | mic = ["olefile"] 681 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 682 | typing = ["typing-extensions"] 683 | xmp = ["defusedxml"] 684 | 685 | [[package]] 686 | name = "pyparsing" 687 | version = "3.2.0" 688 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 689 | optional = false 690 | python-versions = ">=3.9" 691 | files = [ 692 | {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, 693 | {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, 694 | ] 695 | 696 | [package.extras] 697 | diagrams = ["jinja2", "railroad-diagrams"] 698 | 699 | [[package]] 700 | name = "python-dateutil" 701 | version = "2.9.0.post0" 702 | description = "Extensions to the standard Python datetime module" 703 | optional = false 704 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 705 | files = [ 706 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 707 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 708 | ] 709 | 710 | [package.dependencies] 711 | six = ">=1.5" 712 | 713 | [[package]] 714 | name = "pytz" 715 | version = "2024.2" 716 | description = "World timezone definitions, modern and historical" 717 | optional = false 718 | python-versions = "*" 719 | files = [ 720 | {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, 721 | {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, 722 | ] 723 | 724 | [[package]] 725 | name = "scipy" 726 | version = "1.14.1" 727 | description = "Fundamental algorithms for scientific computing in Python" 728 | optional = false 729 | python-versions = ">=3.10" 730 | files = [ 731 | {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, 732 | {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, 733 | {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, 734 | {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, 735 | {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, 736 | {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, 737 | {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, 738 | {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, 739 | {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, 740 | {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, 741 | {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, 742 | {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, 743 | {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, 744 | {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, 745 | {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, 746 | {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, 747 | {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, 748 | {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, 749 | {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, 750 | {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, 751 | {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, 752 | {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, 753 | {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, 754 | {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, 755 | {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, 756 | {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, 757 | {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, 758 | {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, 759 | {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, 760 | {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, 761 | {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, 762 | {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, 763 | {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, 764 | ] 765 | 766 | [package.dependencies] 767 | numpy = ">=1.23.5,<2.3" 768 | 769 | [package.extras] 770 | dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] 771 | doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] 772 | test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] 773 | 774 | [[package]] 775 | name = "six" 776 | version = "1.17.0" 777 | description = "Python 2 and 3 compatibility utilities" 778 | optional = false 779 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 780 | files = [ 781 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 782 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 783 | ] 784 | 785 | [[package]] 786 | name = "smmap" 787 | version = "5.0.1" 788 | description = "A pure Python implementation of a sliding window memory map manager" 789 | optional = false 790 | python-versions = ">=3.7" 791 | files = [ 792 | {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, 793 | {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, 794 | ] 795 | 796 | [[package]] 797 | name = "tzdata" 798 | version = "2024.2" 799 | description = "Provider of IANA time zone data" 800 | optional = false 801 | python-versions = ">=2" 802 | files = [ 803 | {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, 804 | {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, 805 | ] 806 | 807 | [[package]] 808 | name = "xarray" 809 | version = "2024.11.0" 810 | description = "N-D labeled arrays and datasets in Python" 811 | optional = false 812 | python-versions = ">=3.10" 813 | files = [ 814 | {file = "xarray-2024.11.0-py3-none-any.whl", hash = "sha256:6ee94f63ddcbdd0cf3909d1177f78cdac756640279c0e32ae36819a89cdaba37"}, 815 | {file = "xarray-2024.11.0.tar.gz", hash = "sha256:1ccace44573ddb862e210ad3ec204210654d2c750bec11bbe7d842dfc298591f"}, 816 | ] 817 | 818 | [package.dependencies] 819 | numpy = ">=1.24" 820 | packaging = ">=23.2" 821 | pandas = ">=2.1" 822 | 823 | [package.extras] 824 | accel = ["bottleneck", "flox", "numba (>=0.54)", "numbagg", "opt_einsum", "scipy"] 825 | complete = ["xarray[accel,etc,io,parallel,viz]"] 826 | dev = ["hypothesis", "jinja2", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-timeout", "pytest-xdist", "ruff", "sphinx", "sphinx_autosummary_accessors", "xarray[complete]"] 827 | etc = ["sparse"] 828 | io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] 829 | parallel = ["dask[complete]"] 830 | viz = ["cartopy", "matplotlib", "nc-time-axis", "seaborn"] 831 | 832 | [metadata] 833 | lock-version = "2.0" 834 | python-versions = ">=3.10,<4.0" 835 | content-hash = "d3a43a2985996928958c8bd1ba5c221c760a5f86ba80696a368773d5c3f29bd5" 836 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "OLBtools" 3 | version = "0.1.0" 4 | description = "An optical link budget tool, with support for scintillation, APDs, and 4 quadrant detectors" 5 | authors = ["Paul Serra "] 6 | readme = "README.md" 7 | packages = [{include = "OLBtools"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.10,<4.0" 11 | matplotlib = "^3.9.3" 12 | gitpython = "^3.1.43" 13 | numpy = "^2.2.0" 14 | scipy = "^1.14.1" 15 | lowtran = "^3.1.0" 16 | mpmath = "^1.3.0" 17 | pdfkit = "^1.0.0" 18 | 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | --------------------------------------------------------------------------------