AeroViz 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of AeroViz might be problematic. Click here for more details.
- AeroViz/data/240228_00.txt +101 -0
- AeroViz/plot/__init__.py +1 -0
- AeroViz/plot/hysplit/__init__.py +1 -0
- AeroViz/plot/hysplit/hysplit.py +79 -0
- AeroViz/plot/optical/PyMieScatt_update.py +567 -0
- AeroViz/plot/optical/mie_theory.py +260 -0
- AeroViz/plot/optical/optical.py +60 -59
- AeroViz/plot/templates/diurnal_pattern.py +24 -7
- AeroViz/plot/timeseries/template.py +2 -2
- AeroViz/plot/timeseries/timeseries.py +47 -6
- AeroViz/rawDataReader/__init__.py +3 -3
- AeroViz/rawDataReader/core/__init__.py +77 -14
- AeroViz/rawDataReader/script/AE33.py +11 -6
- AeroViz/rawDataReader/script/AE43.py +10 -5
- AeroViz/rawDataReader/script/Aurora.py +14 -10
- AeroViz/rawDataReader/script/BC1054.py +10 -6
- AeroViz/rawDataReader/script/EPA.py +3 -3
- AeroViz/rawDataReader/script/GRIMM.py +1 -2
- AeroViz/rawDataReader/script/MA350.py +12 -5
- AeroViz/rawDataReader/script/Minion.py +9 -4
- AeroViz/rawDataReader/script/NEPH.py +15 -5
- AeroViz/rawDataReader/script/OCEC.py +39 -15
- AeroViz/rawDataReader/script/TEOM.py +13 -9
- AeroViz/rawDataReader/script/VOC.py +1 -1
- {AeroViz-0.1.7.dist-info → AeroViz-0.1.8.dist-info}/METADATA +11 -9
- {AeroViz-0.1.7.dist-info → AeroViz-0.1.8.dist-info}/RECORD +29 -24
- {AeroViz-0.1.7.dist-info → AeroViz-0.1.8.dist-info}/LICENSE +0 -0
- {AeroViz-0.1.7.dist-info → AeroViz-0.1.8.dist-info}/WHEEL +0 -0
- {AeroViz-0.1.7.dist-info → AeroViz-0.1.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from typing import Sequence, Literal
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from numpy import exp, log, log10, sqrt, pi
|
|
6
|
+
|
|
7
|
+
from .PyMieScatt_update import AutoMieQ
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def Mie_Q(m: complex,
|
|
11
|
+
wavelength: float,
|
|
12
|
+
dp: float | Sequence[float]
|
|
13
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
14
|
+
"""
|
|
15
|
+
Calculate Mie scattering efficiency (Q) for given spherical particle diameter(s).
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
m : complex
|
|
20
|
+
The complex refractive index of the particles.
|
|
21
|
+
wavelength : float
|
|
22
|
+
The wavelength of the incident light (in nm).
|
|
23
|
+
dp : float | Sequence[float]
|
|
24
|
+
Particle diameters (in nm), can be a single value or Sequence object.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
Q_ext : ndarray
|
|
29
|
+
The Mie extinction efficiency for each particle diameter.
|
|
30
|
+
Q_sca : ndarray
|
|
31
|
+
The Mie scattering efficiency for each particle diameter.
|
|
32
|
+
Q_abs : ndarray
|
|
33
|
+
The Mie absorption efficiency for each particle diameter.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> Q_ext, Q_sca, Q_abs = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400])
|
|
38
|
+
"""
|
|
39
|
+
# Ensure dp is a numpy array
|
|
40
|
+
dp = np.atleast_1d(dp)
|
|
41
|
+
|
|
42
|
+
# Transpose for proper unpacking
|
|
43
|
+
Q_ext, Q_sca, Q_abs, g, Q_pr, Q_back, Q_ratio = np.array([AutoMieQ(m, wavelength, _dp) for _dp in dp]).T
|
|
44
|
+
|
|
45
|
+
return Q_ext, Q_sca, Q_abs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def Mie_MEE(m: complex,
|
|
49
|
+
wavelength: float,
|
|
50
|
+
dp: float | Sequence[float],
|
|
51
|
+
density: float
|
|
52
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
53
|
+
"""
|
|
54
|
+
Calculate mass extinction efficiency and other parameters.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
m : complex
|
|
59
|
+
The complex refractive index of the particles.
|
|
60
|
+
wavelength : float
|
|
61
|
+
The wavelength of the incident light.
|
|
62
|
+
dp : float | Sequence[float]
|
|
63
|
+
List of particle sizes or a single value.
|
|
64
|
+
density : float
|
|
65
|
+
The density of particles.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
MEE : ndarray
|
|
70
|
+
The mass extinction efficiency for each particle diameter.
|
|
71
|
+
MSE : ndarray
|
|
72
|
+
The mass scattering efficiency for each particle diameter.
|
|
73
|
+
MAE : ndarray
|
|
74
|
+
The mass absorption efficiency for each particle diameter.
|
|
75
|
+
|
|
76
|
+
Examples
|
|
77
|
+
--------
|
|
78
|
+
>>> MEE, MSE, MAE = Mie_MEE(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400], density=1.2)
|
|
79
|
+
"""
|
|
80
|
+
Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
|
|
81
|
+
|
|
82
|
+
MEE = (3 * Q_ext) / (2 * density * dp) * 1000
|
|
83
|
+
MSE = (3 * Q_sca) / (2 * density * dp) * 1000
|
|
84
|
+
MAE = (3 * Q_abs) / (2 * density * dp) * 1000
|
|
85
|
+
|
|
86
|
+
return MEE, MSE, MAE
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def Mie_PESD(m: complex,
|
|
90
|
+
wavelength: float = 550,
|
|
91
|
+
dp: float | Sequence[float] = None,
|
|
92
|
+
ndp: float | Sequence[float] = None,
|
|
93
|
+
lognormal: bool = False,
|
|
94
|
+
dp_range: tuple = (1, 2500),
|
|
95
|
+
geoMean: float = 200,
|
|
96
|
+
geoStdDev: float = 2,
|
|
97
|
+
numberOfParticles: float = 1e6,
|
|
98
|
+
numberOfBins: int = 167,
|
|
99
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
100
|
+
"""
|
|
101
|
+
Simultaneously calculate "extinction distribution" and "integrated results" using the Mie_Q method.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
m : complex
|
|
106
|
+
The complex refractive index of the particles.
|
|
107
|
+
wavelength : float
|
|
108
|
+
The wavelength of the incident light.
|
|
109
|
+
dp : float | Sequence[float]
|
|
110
|
+
Particle sizes.
|
|
111
|
+
ndp : float | Sequence[float]
|
|
112
|
+
Number concentration from SMPS or APS in the units of dN/dlogdp.
|
|
113
|
+
lognormal : bool, optional
|
|
114
|
+
Whether to use lognormal distribution for ndp. Default is False.
|
|
115
|
+
dp_range : tuple, optional
|
|
116
|
+
Range of particle sizes. Default is (1, 2500) nm.
|
|
117
|
+
geoMean : float, optional
|
|
118
|
+
Geometric mean of the particle size distribution. Default is 200 nm.
|
|
119
|
+
geoStdDev : float, optional
|
|
120
|
+
Geometric standard deviation of the particle size distribution. Default is 2.
|
|
121
|
+
numberOfParticles : float, optional
|
|
122
|
+
Number of particles. Default is 1e6.
|
|
123
|
+
numberOfBins : int, optional
|
|
124
|
+
Number of bins for the lognormal distribution. Default is 167.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
ext_dist : ndarray
|
|
129
|
+
The extinction distribution for the given data.
|
|
130
|
+
sca_dist : ndarray
|
|
131
|
+
The scattering distribution for the given data.
|
|
132
|
+
abs_dist : ndarray
|
|
133
|
+
The absorption distribution for the given data.
|
|
134
|
+
|
|
135
|
+
Notes
|
|
136
|
+
-----
|
|
137
|
+
return in "dext/dlogdp", please make sure input the dNdlogdp data.
|
|
138
|
+
|
|
139
|
+
Examples
|
|
140
|
+
--------
|
|
141
|
+
>>> Ext, Sca, Abs = Mie_PESD(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 500, 1000], ndp=[100, 50, 30, 20])
|
|
142
|
+
"""
|
|
143
|
+
if lognormal:
|
|
144
|
+
dp = np.logspace(log10(dp_range[0]), log10(dp_range[1]), numberOfBins)
|
|
145
|
+
|
|
146
|
+
ndp = numberOfParticles * (1 / (log(geoStdDev) * sqrt(2 * pi)) *
|
|
147
|
+
exp(-(log(dp) - log(geoMean)) ** 2 / (2 * log(geoStdDev) ** 2)))
|
|
148
|
+
|
|
149
|
+
# dN / dlogdp
|
|
150
|
+
ndp = np.atleast_1d(ndp)
|
|
151
|
+
|
|
152
|
+
Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
|
|
153
|
+
|
|
154
|
+
# The 1e-6 here is so that the final value is the same as the unit 1/10^6m.
|
|
155
|
+
Ext = Q_ext * (pi / 4 * dp ** 2) * ndp * 1e-6
|
|
156
|
+
Sca = Q_sca * (pi / 4 * dp ** 2) * ndp * 1e-6
|
|
157
|
+
Abs = Q_abs * (pi / 4 * dp ** 2) * ndp * 1e-6
|
|
158
|
+
|
|
159
|
+
return Ext, Sca, Abs
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def internal(dist: pd.Series,
|
|
163
|
+
dp: float | Sequence[float],
|
|
164
|
+
wavelength: float = 550,
|
|
165
|
+
result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
|
|
166
|
+
) -> np.ndarray:
|
|
167
|
+
"""
|
|
168
|
+
Calculate the extinction distribution by internal mixing model.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
dist : pd.Series
|
|
173
|
+
Particle size distribution data.
|
|
174
|
+
dp : float | Sequence[float]
|
|
175
|
+
Diameter(s) of the particles, either a single value or a sequence.
|
|
176
|
+
wavelength : float, optional
|
|
177
|
+
Wavelength of the incident light, default is 550 nm.
|
|
178
|
+
result_type : {'extinction', 'scattering', 'absorption'}, optional
|
|
179
|
+
Type of result to calculate, defaults to 'extinction'.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
np.ndarray
|
|
184
|
+
Extinction distribution calculated based on the internal mixing model.
|
|
185
|
+
"""
|
|
186
|
+
ext_dist, sca_dist, abs_dist = Mie_PESD(m=complex(dist['n_amb'], dist['k_amb']),
|
|
187
|
+
wavelength=wavelength,
|
|
188
|
+
dp=dp,
|
|
189
|
+
ndp=np.array(dist[:np.size(dp)]))
|
|
190
|
+
|
|
191
|
+
if result_type == 'extinction':
|
|
192
|
+
return ext_dist
|
|
193
|
+
elif result_type == 'scattering':
|
|
194
|
+
return sca_dist
|
|
195
|
+
else:
|
|
196
|
+
return abs_dist
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# return dict(ext=ext_dist, sca=sca_dist, abs=abs_dist)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def external(dist: pd.Series,
|
|
203
|
+
dp: float | Sequence[float],
|
|
204
|
+
wavelength: float = 550,
|
|
205
|
+
result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
|
|
206
|
+
) -> np.ndarray:
|
|
207
|
+
"""
|
|
208
|
+
Calculate the extinction distribution by external mixing model.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
dist : pd.Series
|
|
213
|
+
Particle size distribution data.
|
|
214
|
+
dp : float | Sequence[float]
|
|
215
|
+
Diameter(s) of the particles, either a single value or a sequence.
|
|
216
|
+
wavelength : float, optional
|
|
217
|
+
Wavelength of the incident light, default is 550 nm.
|
|
218
|
+
result_type : {'extinction', 'scattering', 'absorption'}, optional
|
|
219
|
+
Type of result to calculate, defaults to 'extinction'.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
np.ndarray
|
|
224
|
+
Extinction distribution calculated based on the external mixing model.
|
|
225
|
+
"""
|
|
226
|
+
refractive_dic = {'AS_volume_ratio': complex(1.53, 0.00),
|
|
227
|
+
'AN_volume_ratio': complex(1.55, 0.00),
|
|
228
|
+
'OM_volume_ratio': complex(1.54, 0.00),
|
|
229
|
+
'Soil_volume_ratio': complex(1.56, 0.01),
|
|
230
|
+
'SS_volume_ratio': complex(1.54, 0.00),
|
|
231
|
+
'EC_volume_ratio': complex(1.80, 0.54),
|
|
232
|
+
'ALWC_volume_ratio': complex(1.33, 0.00)}
|
|
233
|
+
|
|
234
|
+
ndp = np.array(dist[:np.size(dp)])
|
|
235
|
+
mie_results = (
|
|
236
|
+
Mie_PESD(refractive_dic[_specie], wavelength, dp, dist[_specie] / (1 + dist['ALWC_volume_ratio']) * ndp) for
|
|
237
|
+
_specie in refractive_dic)
|
|
238
|
+
|
|
239
|
+
ext_dist, sca_dist, abs_dist = (np.sum([res[0] for res in mie_results], axis=0),
|
|
240
|
+
np.sum([res[1] for res in mie_results], axis=0),
|
|
241
|
+
np.sum([res[2] for res in mie_results], axis=0))
|
|
242
|
+
|
|
243
|
+
if result_type == 'extinction':
|
|
244
|
+
return ext_dist
|
|
245
|
+
elif result_type == 'scattering':
|
|
246
|
+
return sca_dist
|
|
247
|
+
else:
|
|
248
|
+
return abs_dist
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def core_shell():
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def sensitivity():
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == '__main__':
|
|
260
|
+
result = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100., 200.])
|
AeroViz/plot/optical/optical.py
CHANGED
|
@@ -6,13 +6,14 @@ import numpy as np
|
|
|
6
6
|
# from PyMieScatt import ScatteringFunction
|
|
7
7
|
from matplotlib.pyplot import Figure, Axes
|
|
8
8
|
|
|
9
|
+
from AeroViz.plot.optical.PyMieScatt_update import ScatteringFunction
|
|
10
|
+
from AeroViz.plot.optical.mie_theory import Mie_Q, Mie_MEE, Mie_PESD
|
|
9
11
|
from AeroViz.plot.utils import *
|
|
10
|
-
from temp.process.method.mie_theory import Mie_Q, Mie_MEE, Mie_PESD
|
|
11
12
|
|
|
12
13
|
__all__ = ['Q_plot',
|
|
13
14
|
'RI_couple',
|
|
14
15
|
'RRI_2D',
|
|
15
|
-
|
|
16
|
+
'scattering_phase',
|
|
16
17
|
'response_surface',
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -252,59 +253,59 @@ def RRI_2D(mode: Literal["ext", "sca", "abs"] = 'ext',
|
|
|
252
253
|
return fig, ax
|
|
253
254
|
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
256
|
+
@set_figure
|
|
257
|
+
def scattering_phase(m: complex = 1.55 + 0.01j,
|
|
258
|
+
wave: float = 600,
|
|
259
|
+
dp: float = 200) -> tuple[Figure, Axes]:
|
|
260
|
+
"""
|
|
261
|
+
Generate a polar plot to visualize the scattering phase function.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
m : complex, optional
|
|
266
|
+
The complex refractive index of the scattering medium. Default is 1.55 + 0.01j.
|
|
267
|
+
wave : float, optional
|
|
268
|
+
The wavelength of the incident light in nanometers. Default is 600 nm.
|
|
269
|
+
dp : float, optional
|
|
270
|
+
The particle diameter in nanometers. Default is 200 nm.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
ax : Axes
|
|
275
|
+
Matplotlib Axes object containing the generated polar plot.
|
|
276
|
+
|
|
277
|
+
Examples
|
|
278
|
+
--------
|
|
279
|
+
Example usage of the scattering_phase function:
|
|
280
|
+
|
|
281
|
+
>>> ax = scattering_phase(m=1.55 + 0.01j, wave=600, dp=200)
|
|
282
|
+
"""
|
|
283
|
+
theta, _SL, _SR, _SU = ScatteringFunction(m, wave, dp)
|
|
284
|
+
|
|
285
|
+
SL = np.append(_SL, _SL[::-1])
|
|
286
|
+
SR = np.append(_SR, _SR[::-1])
|
|
287
|
+
SU = np.append(_SU, _SU[::-1])
|
|
288
|
+
|
|
289
|
+
angles = ['0', '60', '120', '180', '240', '300']
|
|
290
|
+
|
|
291
|
+
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
|
|
292
|
+
|
|
293
|
+
theta = np.linspace(0, 2 * np.pi, len(SL))
|
|
294
|
+
|
|
295
|
+
plt.thetagrids(range(0, 360, int(360 / len(angles))), angles)
|
|
296
|
+
|
|
297
|
+
plt.plot(theta, SL, '-', linewidth=2, color='#115162', label='SL')
|
|
298
|
+
plt.fill(theta, SL, '#afe0f5', alpha=0.5)
|
|
299
|
+
plt.plot(theta, SR, '-', linewidth=2, color='#7FAE80', label='SR')
|
|
300
|
+
plt.fill(theta, SR, '#b5e6c5', alpha=0.5)
|
|
301
|
+
plt.plot(theta, SU, '-', linewidth=2, color='#621129', label='SU')
|
|
302
|
+
plt.fill(theta, SU, '#f5afbd', alpha=0.5)
|
|
303
|
+
|
|
304
|
+
plt.legend(loc='best', bbox_to_anchor=(1, 0, 0.2, 1), prop={'weight': 'bold'})
|
|
305
|
+
plt.title(r'$\bf Scattering\ phase\ function$')
|
|
306
|
+
|
|
307
|
+
plt.show()
|
|
308
|
+
return fig, ax
|
|
308
309
|
|
|
309
310
|
|
|
310
311
|
@set_figure
|
|
@@ -368,7 +369,7 @@ def response_surface(real_range=(1.33, 1.7),
|
|
|
368
369
|
ax.plot_surface(real, gmd, ext, rstride=1, cstride=1, cmap=plt.get_cmap('jet'), edgecolor='none')
|
|
369
370
|
|
|
370
371
|
ax.set(xlabel='Real part (n)', ylabel='GMD (nm)', zlabel=Unit('Extinction'),
|
|
371
|
-
title='Sensitive tests of
|
|
372
|
+
title='Sensitive tests of extinction')
|
|
372
373
|
|
|
373
374
|
ax.zaxis.get_offset_text().set_visible(False)
|
|
374
375
|
exponent = math.floor(math.log10(np.max(ext)))
|
|
@@ -381,8 +382,8 @@ def response_surface(real_range=(1.33, 1.7),
|
|
|
381
382
|
|
|
382
383
|
|
|
383
384
|
if __name__ == '__main__':
|
|
384
|
-
|
|
385
|
-
|
|
385
|
+
Q_plot(['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], x='dp', y='MEE')
|
|
386
|
+
Q_plot(['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], x='dp', y='Q')
|
|
386
387
|
|
|
387
|
-
|
|
388
|
+
RI_couple()
|
|
388
389
|
response_surface()
|
|
@@ -15,30 +15,47 @@ def diurnal_pattern(df: DataFrame,
|
|
|
15
15
|
ax: Axes | None = None,
|
|
16
16
|
**kwargs
|
|
17
17
|
) -> tuple[Figure, Axes]:
|
|
18
|
-
if 'hour'
|
|
18
|
+
if 'hour' not in df.columns and 'Hour' not in df.columns:
|
|
19
19
|
df['Hour'] = df.index.hour
|
|
20
20
|
|
|
21
21
|
Hour = range(0, 24)
|
|
22
22
|
mean = df.groupby('Hour')[y].mean()
|
|
23
23
|
std = df.groupby('Hour')[y].std() * std_area
|
|
24
24
|
|
|
25
|
-
fig, ax = plt.subplots(
|
|
25
|
+
fig, ax = plt.subplots() if ax is None else (ax.get_figure(), ax)
|
|
26
26
|
|
|
27
27
|
# Plot Diurnal pattern
|
|
28
|
-
ax.plot(Hour, mean, 'blue')
|
|
29
|
-
ax.fill_between(Hour, y1=mean + std, y2=mean - std, alpha=0.2, color='blue', edgecolor=None)
|
|
28
|
+
ax.plot(Hour, mean, 'blue', zorder=3)
|
|
29
|
+
ax.fill_between(Hour, y1=mean + std, y2=mean - std, alpha=0.2, color='blue', edgecolor=None, zorder=2)
|
|
30
|
+
|
|
31
|
+
# Plot Boxplot for each hour
|
|
32
|
+
bp = ax.boxplot([df[df['Hour'] == h][y].dropna() for h in Hour],
|
|
33
|
+
positions=Hour,
|
|
34
|
+
widths=0.5,
|
|
35
|
+
patch_artist=True,
|
|
36
|
+
showfliers=False,
|
|
37
|
+
zorder=1)
|
|
38
|
+
|
|
39
|
+
# Customize boxplot colors
|
|
40
|
+
for element in ['boxes', 'whiskers', 'fliers', 'means', 'medians', 'caps']:
|
|
41
|
+
plt.setp(bp[element], color='gray')
|
|
42
|
+
|
|
43
|
+
for patch in bp['boxes']:
|
|
44
|
+
patch.set(facecolor='lightgray', alpha=0.5)
|
|
30
45
|
|
|
31
46
|
ax.set(xlabel=kwargs.get('xlabel', 'Hours'),
|
|
32
47
|
ylabel=kwargs.get('ylabel', Unit(y)),
|
|
33
|
-
xlim=kwargs.get('xlim', (0, 23)),
|
|
48
|
+
xlim=kwargs.get('xlim', (-0.5, 23.5)),
|
|
34
49
|
ylim=kwargs.get('ylim', (None, None)),
|
|
35
|
-
xticks=kwargs.get('xticks',
|
|
50
|
+
xticks=kwargs.get('xticks', range(0, 24, 4)),
|
|
51
|
+
xticklabels=kwargs.get('xticklabels', range(0, 24, 4)))
|
|
36
52
|
|
|
37
53
|
ax.tick_params(axis='both', which='major')
|
|
38
54
|
ax.tick_params(axis='x', which='minor')
|
|
39
55
|
ax.xaxis.set_minor_locator(AutoMinorLocator())
|
|
40
56
|
ax.ticklabel_format(axis='y', style='sci', scilimits=(-2, 3), useMathText=True)
|
|
41
57
|
|
|
58
|
+
plt.tight_layout()
|
|
42
59
|
plt.show()
|
|
43
60
|
|
|
44
|
-
return fig, ax
|
|
61
|
+
return fig, ax
|
|
@@ -2,7 +2,7 @@ import matplotlib.pyplot as plt
|
|
|
2
2
|
from matplotlib.pyplot import Figure, Axes
|
|
3
3
|
from pandas import DataFrame
|
|
4
4
|
|
|
5
|
-
from AeroViz.plot.timeseries import timeseries
|
|
5
|
+
from AeroViz.plot.timeseries.timeseries import timeseries
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def timeseries_template(df: DataFrame) -> tuple[Figure, Axes]:
|
|
@@ -40,7 +40,7 @@ def timeseries_template(df: DataFrame) -> tuple[Figure, Axes]:
|
|
|
40
40
|
timeseries(df, y='VC', color='PBLH', style='bar', ax=ax4, bar_kws=dict(cmap='Blues'), set_xaxis_visible=False,
|
|
41
41
|
ylim=[0, 5000])
|
|
42
42
|
|
|
43
|
-
timeseries(df, y='
|
|
43
|
+
timeseries(df, y='PM2.5', color='PM1/PM25', style='scatter', ax=ax5, ylim=[0, None])
|
|
44
44
|
|
|
45
45
|
plt.show()
|
|
46
46
|
|
|
@@ -4,8 +4,9 @@ import matplotlib.pyplot as plt
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from matplotlib.cm import ScalarMappable
|
|
6
6
|
from matplotlib.pyplot import Figure, Axes
|
|
7
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
7
8
|
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
|
|
8
|
-
from pandas import DataFrame, date_range
|
|
9
|
+
from pandas import DataFrame, date_range, Timedelta
|
|
9
10
|
|
|
10
11
|
from AeroViz.plot.utils import *
|
|
11
12
|
|
|
@@ -70,6 +71,40 @@ def _plot(ax, df, _y, _color, plot_kws):
|
|
|
70
71
|
ax.plot(df.index, df[_y], color=_color, **plot_kws)
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
def _wind_arrow(ax, df, y, c, scatter_kws, cbar_kws, inset_kws):
|
|
75
|
+
"""
|
|
76
|
+
Plot wind arrows on a scatter plot.
|
|
77
|
+
|
|
78
|
+
:param ax: matplotlib axes
|
|
79
|
+
:param df: pandas DataFrame
|
|
80
|
+
:param y: column name for wind speed
|
|
81
|
+
:param c: column name for wind direction
|
|
82
|
+
:param scatter_kws: keyword arguments for scatter plot
|
|
83
|
+
:param cbar_kws: keyword arguments for colorbar
|
|
84
|
+
:param inset_kws: keyword arguments for inset axes
|
|
85
|
+
"""
|
|
86
|
+
# First, create a scatter plot
|
|
87
|
+
sc = ax.scatter(df.index, df[y], c=df[c], **scatter_kws)
|
|
88
|
+
|
|
89
|
+
# Add colorbar
|
|
90
|
+
divider = make_axes_locatable(ax)
|
|
91
|
+
cax = divider.append_axes("right", size="2%", pad=0.05)
|
|
92
|
+
plt.colorbar(sc, cax=cax, **cbar_kws)
|
|
93
|
+
|
|
94
|
+
# Add wind arrows
|
|
95
|
+
for idx, row in df.iterrows():
|
|
96
|
+
wind_speed = row[y]
|
|
97
|
+
wind_dir = np.radians(row[c])
|
|
98
|
+
dx = np.sin(wind_dir) * wind_speed / 20 # Scale factor can be adjusted
|
|
99
|
+
dy = np.cos(wind_dir) * wind_speed / 20
|
|
100
|
+
ax.annotate('', xy=(idx + 10 * dx * Timedelta(hours=5), wind_speed + 4 * dy),
|
|
101
|
+
xytext=(idx - 10 * dx * Timedelta(hours=5), wind_speed - 4 * dy),
|
|
102
|
+
arrowprops=dict(arrowstyle='->', color='k', linewidth=0.5))
|
|
103
|
+
|
|
104
|
+
# Set the x-axis limit to show all data points
|
|
105
|
+
# ax.set_xlim(df.index.min() - datetime.timedelta(days=1), df.index.max())
|
|
106
|
+
|
|
107
|
+
|
|
73
108
|
def process_timeseries_data(df, rolling=None, interpolate_limit=None):
|
|
74
109
|
# apply rolling window if specified
|
|
75
110
|
df = df.rolling(window=rolling, min_periods=1).mean(numeric_only=True) if rolling is not None else df
|
|
@@ -90,7 +125,7 @@ def timeseries(df: DataFrame,
|
|
|
90
125
|
interpolate_limit: int | None = 6,
|
|
91
126
|
major_freq: str = '1MS',
|
|
92
127
|
minor_freq: str = '10d',
|
|
93
|
-
style: list[Literal['scatter', 'bar', 'line']] | str | None = None,
|
|
128
|
+
style: list[Literal['scatter', 'bar', 'line', 'arrow']] | str | None = None,
|
|
94
129
|
ax: Axes | None = None,
|
|
95
130
|
set_xaxis_visible: bool | None = None,
|
|
96
131
|
legend_loc: Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'] = 'best',
|
|
@@ -199,16 +234,16 @@ def timeseries(df: DataFrame,
|
|
|
199
234
|
if y2 and ('scatter' or 'bar') in style:
|
|
200
235
|
fig.subplots_adjust(right=0.8)
|
|
201
236
|
|
|
202
|
-
for i, _c in enumerate(color):
|
|
203
|
-
|
|
204
|
-
|
|
237
|
+
# for i, _c in enumerate(color):
|
|
238
|
+
# if _c is not None and _c in df.columns:
|
|
239
|
+
# style[i] = 'scatter'
|
|
205
240
|
|
|
206
241
|
for i, (_y, _c, _label, _style) in enumerate(zip(y, color, label, style)):
|
|
207
242
|
scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws', {})}
|
|
208
243
|
bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws', {})}
|
|
209
244
|
plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws', {})}
|
|
210
245
|
|
|
211
|
-
if _style in ['scatter', 'bar']:
|
|
246
|
+
if _style in ['scatter', 'bar', 'arrow']:
|
|
212
247
|
cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws', {})}
|
|
213
248
|
inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws', {})}
|
|
214
249
|
|
|
@@ -218,6 +253,9 @@ def timeseries(df: DataFrame,
|
|
|
218
253
|
elif _style == 'bar':
|
|
219
254
|
_bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws)
|
|
220
255
|
|
|
256
|
+
elif _style == 'arrow':
|
|
257
|
+
_wind_arrow(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
|
|
258
|
+
|
|
221
259
|
else:
|
|
222
260
|
_plot(ax, df, _y, _c, plot_kws)
|
|
223
261
|
|
|
@@ -237,6 +275,9 @@ def timeseries(df: DataFrame,
|
|
|
237
275
|
elif _style == 'bar':
|
|
238
276
|
_bar(ax2, df, _y, _c, bar_kws, cbar_kws, inset_kws)
|
|
239
277
|
|
|
278
|
+
elif _style == 'arrow':
|
|
279
|
+
pass
|
|
280
|
+
|
|
240
281
|
else: # line plot
|
|
241
282
|
_plot(ax2, df, _y, _c, plot_kws)
|
|
242
283
|
|
|
@@ -15,7 +15,7 @@ SUPPORTED_INSTRUMENTS = [
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def RawDataReader(instrument_name: str,
|
|
18
|
-
path: Path,
|
|
18
|
+
path: Path | str,
|
|
19
19
|
reset: bool = False,
|
|
20
20
|
qc: bool | str = True,
|
|
21
21
|
qc_freq: str | None = None,
|
|
@@ -87,7 +87,7 @@ def RawDataReader(instrument_name: str,
|
|
|
87
87
|
|
|
88
88
|
if start and end:
|
|
89
89
|
if end.hour == 0 and end.minute == 0 and end.second == 0:
|
|
90
|
-
end = end.replace(hour=23)
|
|
90
|
+
end = end.replace(hour=23, minute=59, second=59)
|
|
91
91
|
else:
|
|
92
92
|
raise ValueError("Both start and end times must be provided.")
|
|
93
93
|
if end <= start:
|
|
@@ -98,7 +98,7 @@ def RawDataReader(instrument_name: str,
|
|
|
98
98
|
Timedelta(mean_freq)
|
|
99
99
|
except ValueError:
|
|
100
100
|
raise ValueError(
|
|
101
|
-
f"Invalid mean_freq: '{mean_freq}'. It should be a valid frequency string (e.g., '
|
|
101
|
+
f"Invalid mean_freq: '{mean_freq}'. It should be a valid frequency string (e.g., '1h', '30min', '1D').")
|
|
102
102
|
|
|
103
103
|
# Instantiate the class and return the instance
|
|
104
104
|
reader_module = instrument_class_map[instrument_name].Reader(
|