AeroViz 0.1.0__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.

Files changed (102) hide show
  1. AeroViz/__init__.py +15 -0
  2. AeroViz/dataProcess/Chemistry/__init__.py +63 -0
  3. AeroViz/dataProcess/Chemistry/_calculate.py +27 -0
  4. AeroViz/dataProcess/Chemistry/_isoropia.py +99 -0
  5. AeroViz/dataProcess/Chemistry/_mass_volume.py +175 -0
  6. AeroViz/dataProcess/Chemistry/_ocec.py +184 -0
  7. AeroViz/dataProcess/Chemistry/_partition.py +29 -0
  8. AeroViz/dataProcess/Chemistry/_teom.py +16 -0
  9. AeroViz/dataProcess/Optical/_IMPROVE.py +61 -0
  10. AeroViz/dataProcess/Optical/__init__.py +62 -0
  11. AeroViz/dataProcess/Optical/_absorption.py +54 -0
  12. AeroViz/dataProcess/Optical/_extinction.py +36 -0
  13. AeroViz/dataProcess/Optical/_mie.py +16 -0
  14. AeroViz/dataProcess/Optical/_mie_sd.py +143 -0
  15. AeroViz/dataProcess/Optical/_scattering.py +30 -0
  16. AeroViz/dataProcess/SizeDistr/__init__.py +61 -0
  17. AeroViz/dataProcess/SizeDistr/__merge.py +250 -0
  18. AeroViz/dataProcess/SizeDistr/_merge.py +245 -0
  19. AeroViz/dataProcess/SizeDistr/_merge_v1.py +254 -0
  20. AeroViz/dataProcess/SizeDistr/_merge_v2.py +243 -0
  21. AeroViz/dataProcess/SizeDistr/_merge_v3.py +518 -0
  22. AeroViz/dataProcess/SizeDistr/_merge_v4.py +424 -0
  23. AeroViz/dataProcess/SizeDistr/_size_distr.py +93 -0
  24. AeroViz/dataProcess/VOC/__init__.py +19 -0
  25. AeroViz/dataProcess/VOC/_potential_par.py +76 -0
  26. AeroViz/dataProcess/__init__.py +11 -0
  27. AeroViz/dataProcess/core/__init__.py +92 -0
  28. AeroViz/plot/__init__.py +7 -0
  29. AeroViz/plot/distribution/__init__.py +1 -0
  30. AeroViz/plot/distribution/distribution.py +582 -0
  31. AeroViz/plot/improve/__init__.py +1 -0
  32. AeroViz/plot/improve/improve.py +240 -0
  33. AeroViz/plot/meteorology/__init__.py +1 -0
  34. AeroViz/plot/meteorology/meteorology.py +317 -0
  35. AeroViz/plot/optical/__init__.py +2 -0
  36. AeroViz/plot/optical/aethalometer.py +77 -0
  37. AeroViz/plot/optical/optical.py +388 -0
  38. AeroViz/plot/templates/__init__.py +8 -0
  39. AeroViz/plot/templates/contour.py +47 -0
  40. AeroViz/plot/templates/corr_matrix.py +108 -0
  41. AeroViz/plot/templates/diurnal_pattern.py +42 -0
  42. AeroViz/plot/templates/event_evolution.py +65 -0
  43. AeroViz/plot/templates/koschmieder.py +156 -0
  44. AeroViz/plot/templates/metal_heatmap.py +57 -0
  45. AeroViz/plot/templates/regression.py +256 -0
  46. AeroViz/plot/templates/scatter.py +130 -0
  47. AeroViz/plot/templates/templates.py +398 -0
  48. AeroViz/plot/timeseries/__init__.py +1 -0
  49. AeroViz/plot/timeseries/timeseries.py +317 -0
  50. AeroViz/plot/utils/__init__.py +3 -0
  51. AeroViz/plot/utils/_color.py +71 -0
  52. AeroViz/plot/utils/_decorator.py +74 -0
  53. AeroViz/plot/utils/_unit.py +55 -0
  54. AeroViz/process/__init__.py +31 -0
  55. AeroViz/process/core/DataProc.py +19 -0
  56. AeroViz/process/core/SizeDist.py +90 -0
  57. AeroViz/process/core/__init__.py +4 -0
  58. AeroViz/process/method/PyMieScatt_update.py +567 -0
  59. AeroViz/process/method/__init__.py +2 -0
  60. AeroViz/process/method/mie_theory.py +258 -0
  61. AeroViz/process/method/prop.py +62 -0
  62. AeroViz/process/script/AbstractDistCalc.py +143 -0
  63. AeroViz/process/script/Chemical.py +176 -0
  64. AeroViz/process/script/IMPACT.py +49 -0
  65. AeroViz/process/script/IMPROVE.py +161 -0
  66. AeroViz/process/script/Others.py +65 -0
  67. AeroViz/process/script/PSD.py +103 -0
  68. AeroViz/process/script/PSD_dry.py +94 -0
  69. AeroViz/process/script/__init__.py +5 -0
  70. AeroViz/process/script/retrieve_RI.py +70 -0
  71. AeroViz/rawDataReader/__init__.py +68 -0
  72. AeroViz/rawDataReader/core/__init__.py +397 -0
  73. AeroViz/rawDataReader/script/AE33.py +31 -0
  74. AeroViz/rawDataReader/script/AE43.py +34 -0
  75. AeroViz/rawDataReader/script/APS_3321.py +47 -0
  76. AeroViz/rawDataReader/script/Aurora.py +38 -0
  77. AeroViz/rawDataReader/script/BC1054.py +46 -0
  78. AeroViz/rawDataReader/script/EPA_vertical.py +18 -0
  79. AeroViz/rawDataReader/script/GRIMM.py +35 -0
  80. AeroViz/rawDataReader/script/IGAC_TH.py +104 -0
  81. AeroViz/rawDataReader/script/IGAC_ZM.py +90 -0
  82. AeroViz/rawDataReader/script/MA350.py +45 -0
  83. AeroViz/rawDataReader/script/NEPH.py +57 -0
  84. AeroViz/rawDataReader/script/OCEC_LCRES.py +34 -0
  85. AeroViz/rawDataReader/script/OCEC_RES.py +28 -0
  86. AeroViz/rawDataReader/script/SMPS_TH.py +41 -0
  87. AeroViz/rawDataReader/script/SMPS_aim11.py +51 -0
  88. AeroViz/rawDataReader/script/SMPS_genr.py +51 -0
  89. AeroViz/rawDataReader/script/TEOM.py +46 -0
  90. AeroViz/rawDataReader/script/Table.py +28 -0
  91. AeroViz/rawDataReader/script/VOC_TH.py +30 -0
  92. AeroViz/rawDataReader/script/VOC_ZM.py +37 -0
  93. AeroViz/rawDataReader/script/__init__.py +22 -0
  94. AeroViz/tools/__init__.py +3 -0
  95. AeroViz/tools/database.py +94 -0
  96. AeroViz/tools/dataclassifier.py +117 -0
  97. AeroViz/tools/datareader.py +66 -0
  98. AeroViz-0.1.0.dist-info/LICENSE +21 -0
  99. AeroViz-0.1.0.dist-info/METADATA +117 -0
  100. AeroViz-0.1.0.dist-info/RECORD +102 -0
  101. AeroViz-0.1.0.dist-info/WHEEL +5 -0
  102. AeroViz-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,240 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ import pandas as pd
4
+ from matplotlib.pyplot import Figure, Axes
5
+ from pandas import DataFrame, read_json
6
+ from scipy.optimize import curve_fit
7
+ from pathlib import Path
8
+
9
+ from AeroViz import plot
10
+ from AeroViz.plot.utils import *
11
+ from AeroViz.tools import DataBase, DataReader, DataClassifier
12
+
13
+ # TODO: this file has to be reorganized
14
+
15
+ __all__ = ['chemical_enhancement',
16
+ 'ammonium_rich',
17
+ 'pie_IMPROVE',
18
+ 'MLR_IMPROVE',
19
+ 'fRH_plot',
20
+ ]
21
+
22
+
23
+ @set_figure
24
+ def chemical_enhancement(data_set: DataFrame = None,
25
+ data_std: DataFrame = None,
26
+ ax: Axes | None = None,
27
+ **kwargs
28
+ ) -> tuple[Figure, Axes]:
29
+ fig, ax = plt.subplots() if ax is None else (ax.get_figure(), ax)
30
+
31
+ ser_grp_sta, ser_grp_sta_std = DataClassifier(DataBase('/Users/chanchihyu/NTU/2020能見度計畫/data/All_data.csv'),
32
+ by='State')
33
+ species = ['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC', 'ALWC']
34
+ data_set, data_std = ser_grp_sta.loc[:, species], ser_grp_sta_std.loc[:, species]
35
+
36
+ width = 0.20
37
+ block = width / 4
38
+
39
+ x = np.array([1, 2, 3, 4, 5, 6, 7])
40
+ for i, state in enumerate(['Clean', 'Transition', 'Event']):
41
+ val = np.array(data_set.iloc[i, :-1])
42
+ std = (0,) * 7, np.array(data_std.iloc[i, :-1])
43
+
44
+ plt.bar(x + (i + 1) * (width + block), val, yerr=std, width=width, color=Color.colors3[:-1],
45
+ alpha=0.6 + (0.2 * i),
46
+ edgecolor=None, capsize=None, label=state)
47
+
48
+ ax.set(xlabel=r'$\bf Chemical\ species$',
49
+ ylabel=r'$\bf Mass\ concentration\ ({\mu}g/m^3)$',
50
+ xticks=x + 2 * (width + block),
51
+ xticklabels=species,
52
+ ylim=(0, 25),
53
+ title=r'$\bf Chemical\ enhancement$')
54
+
55
+ ax.vlines(8, 0, 25, linestyles='--', colors='k')
56
+
57
+ ax2 = ax.twinx()
58
+ for i, state in enumerate(['Clean', 'Transition', 'Event']):
59
+ val = np.array(data_set.iloc[i, -1])
60
+ std = np.array([[0], [data_std.iloc[i, -1]]])
61
+ plt.bar(8 + (i + 1) * (width + block), val, yerr=std, width=width, color='#96c8e6',
62
+ alpha=0.6 + (0.2 * i), edgecolor=None, capsize=None, label=state)
63
+
64
+ ax2.set(ylabel=r'$\bf Mass\ concentration\ ({\mu}g/m^3)$',
65
+ ylim=(0, 100),
66
+ xticks=x + 2 * (width + block),
67
+ xticklabels=species
68
+ )
69
+
70
+ a = (np.array(data_set.loc['Event']) + np.array(data_set.loc['Transition'])) / 2
71
+ b = (np.array(data_set.loc['Transition']) + np.array(data_set.loc['Clean'])) / 2
72
+ c = np.array(data_set.loc['Event']) / np.array(data_set.loc['Transition'])
73
+ d = np.array(data_set.loc['Transition']) / np.array(data_set.loc['Clean'])
74
+
75
+ for i, (posa, posb, vala, valb) in enumerate(zip(a, b, c, d)):
76
+ if i < 7:
77
+ ax.text(i + 1.5, posa, '{:.2f}'.format(vala), fontsize=6, weight='bold', zorder=1)
78
+ ax.text(i + 1.25, posb, '{:.2f}'.format(valb), fontsize=6, weight='bold', zorder=1)
79
+ else:
80
+ ax2.text(i + 1.5, posa, '{:.2f}'.format(vala), fontsize=6, weight='bold', zorder=1)
81
+ ax2.text(i + 1.25, posb, '{:.2f}'.format(valb), fontsize=6, weight='bold', zorder=1)
82
+
83
+ plt.show()
84
+
85
+ return fig, ax
86
+
87
+
88
+ @set_figure
89
+ def ammonium_rich(df: DataFrame,
90
+ **kwargs
91
+ ) -> tuple[Figure, Axes]:
92
+ df = df[['NH4+', 'SO42-', 'NO3-', 'PM25']].dropna().copy().div([18, 96, 62, 1])
93
+ df['required_ammonium'] = df['NO3-'] + 2 * df['SO42-']
94
+
95
+ fig, ax = plt.subplots()
96
+
97
+ scatter = ax.scatter(df['required_ammonium'].to_numpy(), df['NH4+'].to_numpy(), c=df['PM25'].to_numpy(),
98
+ vmin=0, vmax=70, cmap='jet', marker='o', s=10, alpha=1)
99
+
100
+ ax.axline((0, 0), slope=1., color='k', lw=2, ls='--', alpha=0.5, label='1:1')
101
+ plt.text(0.97, 0.97, r'$\bf 1:1\ Line$', color='k', ha='right', va='top', transform=ax.transAxes)
102
+
103
+ ax.set(xlim=(0, 1.2),
104
+ ylim=(0, 1.2),
105
+ xlabel=r'$\bf NO_{3}^{-}\ +\ 2\ \times\ SO_{4}^{2-}\ (mole\ m^{-3})$',
106
+ ylabel=r'$\bf NH_{4}^{+}\ (mole\ m^{-3})$',
107
+ title=kwargs.get('title', ''))
108
+
109
+ color_bar = plt.colorbar(scatter, label=Unit('PM25'), extend='both')
110
+
111
+ # fig.savefig(f'Ammonium_rich_{title}')
112
+ plt.show()
113
+
114
+ return fig, ax
115
+
116
+
117
+ def pie_IMPROVE():
118
+ Species1 = ['AS_ext_dry', 'AN_ext_dry', 'OM_ext_dry', 'Soil_ext_dry', 'SS_ext_dry', 'EC_ext_dry']
119
+ Species2 = ['AS_ext_dry', 'AN_ext_dry', 'OM_ext_dry', 'Soil_ext_dry', 'SS_ext_dry', 'EC_ext_dry', 'ALWC_ext']
120
+ Species3 = ['AS_ext', 'AN_ext', 'OM_ext', 'Soil_ext', 'SS_ext', 'EC_ext']
121
+
122
+ ser_grp_sta, _ = DataClassifier(DataBase(), by='State')
123
+
124
+ ext_dry_dict = ser_grp_sta.loc[:, Species1]
125
+ ext_amb_dict = ser_grp_sta.loc[:, Species2]
126
+ ext_mix_dict = ser_grp_sta.loc[:, Species3]
127
+
128
+ plot.donuts(data_set=ext_dry_dict, labels=['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], unit='Extinction')
129
+ plot.donuts(data_set=ext_mix_dict, labels=['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], unit='Extinction')
130
+ plot.donuts(data_set=ext_amb_dict, labels=['AS', 'AN', 'OM', 'Soil', 'SS', 'BC', 'ALWC'],
131
+ unit='Extinction', colors=Color.colors2)
132
+
133
+
134
+ def MLR_IMPROVE(**kwargs):
135
+ """
136
+ Perform multiple linear regression analysis and generate plots based on IMPROVE dataset.
137
+
138
+ Parameters
139
+ ----------
140
+ **kwargs : dict
141
+ Additional keyword arguments for customization.
142
+
143
+ Returns
144
+ -------
145
+ None
146
+
147
+ Examples
148
+ --------
149
+ Example usage of MLR_IMPROVE function:
150
+
151
+ >>> MLR_IMPROVE()
152
+
153
+ Notes
154
+ -----
155
+ This function performs multiple linear regression analysis on the IMPROVE dataset and generates plots for analysis.
156
+
157
+ - The function first selects specific species from the dataset and drops NaN values.
158
+ - It calculates a 'Localized' value based on a multiplier and the sum of selected species.
159
+ - Data from 'modified_IMPROVE.csv' and 'revised_IMPROVE.csv' are read and concatenated with the dataset.
160
+ - Statistical analysis is performed using DataClassifier to calculate mean and standard deviation.
161
+ - Plots are generated using linear_regression for Extinction vs. Revised/Modified/Localized and Pie.donuts for a
162
+ pie chart showing the distribution of species based on Extinction.
163
+
164
+ """
165
+ species = ['Extinction', 'Scattering', 'Absorption',
166
+ 'total_ext_dry', 'AS_ext_dry', 'AN_ext_dry', 'OM_ext_dry', 'Soil_ext_dry', 'SS_ext_dry', 'EC_ext_dry',
167
+ 'AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC', 'OM']
168
+
169
+ df = DataBase('/Users/chanchihyu/NTU/2020能見度計畫/data/All_data.csv')[species].dropna().copy()
170
+
171
+ # multiple_linear_regression(df, x=['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS'], y='Scattering', add_constant=True)
172
+ # multiple_linear_regression(df, x=['POC', 'SOC', 'EC'], y='Absorption', add_constant=True)
173
+ # multiple_linear_regression(df, x=['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC'], y='Extinction', add_constant=False)
174
+
175
+ multiplier = [2.675, 4.707, 11.6, 7.272, 0, 0.131, 10.638]
176
+ df['Localized'] = df[['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC']].mul(multiplier).sum(axis=1)
177
+ # TODO: remove name
178
+ modify_IMPROVE = DataReader('modified_IMPROVE.csv')['total_ext_dry'].rename('Modified')
179
+ revised_IMPROVE = DataReader('revised_IMPROVE.csv')['total_ext_dry'].rename('Revised')
180
+
181
+ df = pd.concat([df, revised_IMPROVE, modify_IMPROVE], axis=1)
182
+
183
+ n_df = df[['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC']].mul(multiplier)
184
+ mean, std = DataClassifier(n_df, 'State')
185
+
186
+ ser_grp_sta, _ = DataClassifier(DataBase(), by='State')
187
+ mass_comp = ser_grp_sta.loc[:, ['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC']]
188
+
189
+ # plot
190
+ plot.linear_regression(df, x='Extinction', y=['Revised', 'Modified', 'Localized'], xlim=[0, 400], ylim=[0, 400],
191
+ regression=True, diagonal=True)
192
+ plot.donuts(data_set=mass_comp, labels=['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC'],
193
+ unit='PM25', colors=Color.colors3)
194
+ plot.donuts(mean, labels=['AS', 'AN', 'POC', 'SOC', 'Soil', 'SS', 'EC'], unit='Extinction', colors=Color.colors3)
195
+
196
+
197
+ @set_figure
198
+ def fRH_plot(**kwargs) -> tuple[Figure, Axes]:
199
+ frh = read_json(Path(__file__).parent.parent / 'utils' / 'fRH.json')
200
+
201
+ def fitting_func(RH, a, b, c):
202
+ f = a + b * (RH / 100) ** c
203
+ return f
204
+
205
+ x = frh.index.to_numpy()
206
+ y = frh['fRHs'].to_numpy()
207
+
208
+ result = curve_fit(fitting_func, x, y)
209
+ params = result[0].tolist()
210
+ val_fit = fitting_func(x, *params)
211
+
212
+ fig, ax = plt.subplots(figsize=(3, 3))
213
+
214
+ ax.plot(frh.index, frh['fRH'], 'k-o', ms=2, label='$f(RH)_{original}$')
215
+ ax.plot(frh.index, frh['fRHs'], 'g-o', ms=2, label='$f(RH)_{small\\ mode}$')
216
+ ax.plot(frh.index, frh['fRHl'], 'r-o', ms=2, label='$f(RH)_{large\\ mode}$')
217
+ ax.plot(frh.index, frh['fRHSS'], 'b-o', ms=2, label='$f(RH)_{sea\\ salt}$')
218
+
219
+ ax.set(xlim=(0, 100),
220
+ ylim=(1, None),
221
+ xlabel='$RH (\\%)$',
222
+ ylabel='$f(RH)$',
223
+ title=f'$Hygroscopic\\ growth\\ factor$'
224
+ )
225
+
226
+ ax.grid(axis='y', color='gray', linestyle='dashed', linewidth=0.4, alpha=0.4)
227
+
228
+ ax.legend()
229
+
230
+ plt.show()
231
+ # fig.savefig('fRH_plot')
232
+
233
+ return fig, ax
234
+
235
+
236
+ if __name__ == '__main__':
237
+ # chemical_enhancement()
238
+ # MLR_IMPROVE()
239
+ # ammonium_rich()
240
+ fRH_plot()
@@ -0,0 +1 @@
1
+ from .meteorology import *
@@ -0,0 +1,317 @@
1
+ import math
2
+ from typing import Literal
3
+
4
+ import matplotlib.colors as plc
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ import pandas as pd
8
+ import seaborn as sns
9
+ import windrose
10
+ from matplotlib.pyplot import Figure, Axes
11
+ from pandas import DataFrame, Series
12
+ from scipy.ndimage import gaussian_filter
13
+
14
+ from AeroViz.plot.utils import *
15
+
16
+ __all__ = ['wind_tms',
17
+ 'wind_rose',
18
+ 'CBPF'
19
+ ]
20
+
21
+
22
+ @set_figure(fs=6)
23
+ def wind_tms(df: DataFrame,
24
+ WS: Series | str,
25
+ WD: Series | str,
26
+ **kwargs
27
+ ) -> tuple[Figure, Axes]:
28
+ def drawArrow(A, B, ax: plt.Axes): # 畫箭頭
29
+ _ax = ax.twinx()
30
+ if A[0] == B[0] and A[1] == B[1]: # 靜風畫點
31
+ _ax.plot(A[0], A[1], 'ko')
32
+ else:
33
+ _ax.annotate("", xy=(B[0], B[1]), xytext=(A[0], A[1]), arrowprops=dict(arrowstyle="->"))
34
+
35
+ _ax.spines['left'].set_visible(False)
36
+ _ax.spines['right'].set_visible(False)
37
+ _ax.spines['top'].set_visible(False)
38
+ _ax.spines['bottom'].set_visible(False)
39
+ _ax.set_xlim(0, )
40
+ _ax.set_ylim(0, 5)
41
+ _ax.get_yaxis().set_visible(False)
42
+ _ax.set_aspect('equal') # x轴y轴等比例
43
+
44
+ _ax.tick_params(axis='x', rotation=90)
45
+ ax.tick_params(axis='x', rotation=90)
46
+ plt.tight_layout()
47
+
48
+ fig, ax = plt.subplots(figsize=(8, 2))
49
+ uniform_data = [WS]
50
+ colors = ['lightskyblue', 'darkturquoise', 'lime', 'greenyellow', 'orangered', 'red']
51
+ clrmap = plc.LinearSegmentedColormap.from_list("mycmap", colors) # 自定义色标
52
+ sns.heatmap(uniform_data, square=True, annot=True, fmt=".2f", linewidths=.5, cmap=clrmap,
53
+ yticklabels=['Wind speed (m/s)'], xticklabels=kwargs.get('xticklabels', None), cbar=False, vmin=0,
54
+ vmax=5, ax=ax)
55
+ ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
56
+ ax.set_yticklabels(ax.get_yticklabels(), rotation=0)
57
+ ax.spines['bottom'].set_position(('data', 1)) # 移动x轴
58
+
59
+ for idx, (x, value) in enumerate(WD.items()):
60
+ if not pd.isna(value):
61
+ a = np.array([0.5 + 0.5 * np.sin(value / 180 * np.pi) + idx, 3.5 + 0.5 * np.cos(value / 180 * np.pi)])
62
+ b = np.array([0.5 - 0.5 * np.sin(value / 180 * np.pi) + idx, 3.5 - 0.5 * np.cos(value / 180 * np.pi)])
63
+ drawArrow(a, b, ax)
64
+ else:
65
+ a = np.array([0.5 + idx, 3.5])
66
+ drawArrow(a, a, ax)
67
+
68
+ plt.show()
69
+
70
+ return fig, ax
71
+
72
+
73
+ @set_figure(figsize=(4.3, 4))
74
+ def wind_rose(df: DataFrame,
75
+ WS: Series | str,
76
+ WD: Series | str,
77
+ val: Series | str | None = None,
78
+ typ: Literal['bar', 'scatter'] = 'scatter',
79
+ rlabel_pos: float = 30,
80
+ **kwargs
81
+ ) -> tuple[Figure, Axes]:
82
+ # conditional bivariate probability function (cbpf) python
83
+ # https://davidcarslaw.github.io/openair/reference/polarPlot.html
84
+ # https://github.com/davidcarslaw/openair/blob/master/R/polarPlot.R
85
+ windrose.WindroseAxes._info = 'WindroseAxes'
86
+
87
+ df = df.dropna(subset=[WS, WD] + ([val] if val is not None else []))
88
+
89
+ radius = df[WS].to_numpy()
90
+ theta = df[WD].to_numpy()
91
+ radian = np.radians(theta)
92
+ values = df[val].to_numpy() if val is not None else None
93
+
94
+ # In this case, the windrose is a simple frequency diagram,
95
+ # the function automatically calculates the radians of the given wind direction.
96
+ if typ == 'bar':
97
+ fig, ax = plt.subplots(figsize=(5.5, 4), subplot_kw={'projection': 'windrose'})
98
+ fig.subplots_adjust(left=0)
99
+
100
+ ax.bar(theta, radius, bins=[0, 1, 2, 3], normed=True, colors=['#0F1035', '#365486', '#7FC7D9', '#DCF2F1'])
101
+ ax.set(
102
+ ylim=(0, 30),
103
+ yticks=[0, 15, 30],
104
+ yticklabels=['', '15 %', '30 %'],
105
+ rlabel_position=rlabel_pos
106
+ )
107
+ ax.set_thetagrids(angles=[0, 45, 90, 135, 180, 225, 270, 315],
108
+ labels=["E", "NE", "N", "NW", "W", "SW", "S", "SE"])
109
+
110
+ ax.legend(units='m/s', bbox_to_anchor=[1.1, 0.5], loc='center left', ncol=1)
111
+
112
+ # In this case, the windrose is a scatter plot,
113
+ # in contrary, this function does not calculate the radians, so user have to input the radian.
114
+ else:
115
+ fig, ax = plt.subplots(figsize=(5, 4), subplot_kw={'projection': 'windrose'})
116
+ fig.subplots_adjust(left=0)
117
+
118
+ scatter = ax.scatter(radian, radius, s=15, c=values, vmax=np.quantile(values, 0.90), edgecolors='none',
119
+ cmap='jet', alpha=0.8)
120
+ ax.set(
121
+ ylim=(0, 7),
122
+ yticks=[1, 3, 5, 7],
123
+ yticklabels=['1 m/s', '3 m/s', '5 m/s', '7 m/s'],
124
+ rlabel_position=rlabel_pos,
125
+ theta_direction=-1,
126
+ theta_zero_location='N',
127
+ )
128
+ ax.set_thetagrids(angles=[0, 45, 90, 135, 180, 225, 270, 315],
129
+ labels=["N", "NE", "E", "SE", "S", "SW", "W", "NW"])
130
+
131
+ plt.colorbar(scatter, ax=ax, label=Unit(val), pad=0.1, fraction=0.04)
132
+
133
+ plt.show()
134
+
135
+ return fig, ax
136
+
137
+
138
+ @set_figure(figsize=(4.3, 4))
139
+ def CBPF(df: DataFrame,
140
+ WS: Series | str,
141
+ WD: Series | str,
142
+ val: Series | str | None = None,
143
+ percentile: list | float | int | None = None,
144
+ max_ws: float | None = 5,
145
+ resolution: int = 100,
146
+ sigma: float | tuple = 2,
147
+ rlabel_pos: float = 30,
148
+ bottom_text: str | bool | None = None,
149
+ **kwargs
150
+ ) -> tuple[Figure, Axes]:
151
+ # conditional bivariate probability function (cbpf) python
152
+ # https://davidcarslaw.github.io/openair/reference/polarPlot.html
153
+ # https://github.com/davidcarslaw/openair/blob/master/R/polarPlot.R
154
+
155
+ df = df.dropna(subset=[WS, WD] + ([val] if val is not None else [])).copy()
156
+
157
+ df['u'] = df[WS].to_numpy() * np.sin(np.radians(df[WD].to_numpy()))
158
+ df['v'] = df[WS].to_numpy() * np.cos(np.radians(df[WD].to_numpy()))
159
+
160
+ u_bins = np.linspace(df.u.min(), df.u.max(), resolution)
161
+ v_bins = np.linspace(df.v.min(), df.v.max(), resolution)
162
+
163
+ # 使用 u_group 和 v_group 進行分組
164
+ df['u_group'] = pd.cut(df['u'], u_bins)
165
+ df['v_group'] = pd.cut(df['v'], v_bins)
166
+ grouped = df.groupby(['u_group', 'v_group'], observed=False)
167
+
168
+ X, Y = np.meshgrid(u_bins, v_bins)
169
+
170
+ # Note:
171
+ # The CBPF is the ratio between the number of points in each cell and the total number of points.
172
+ # So, it is not equal to the probability density function (PDF) of the wind speed and wind direction.
173
+
174
+ if percentile is None:
175
+ histogram = (grouped[val].count() / grouped[val].count().sum()).unstack().values.T
176
+ # histogram, v_edges, u_edges = np.histogram2d(df.v, df.u, bins=(v_bins, u_bins))
177
+ # histogram = histogram / histogram.sum()
178
+ histogram = np.where(histogram == 0, np.nan, histogram)
179
+ bottom_text = rf'$PDF\ plot$'
180
+
181
+ else:
182
+ if not all(0 <= p <= 100 for p in (percentile if isinstance(percentile, list) else [percentile])):
183
+ raise ValueError("Percentile must be between 0 and 100")
184
+
185
+ if isinstance(percentile, (float, int)):
186
+ bottom_text = rf'$CPF:\ >{int(percentile)}^{{th}}$'
187
+ thershold = df[val].quantile(percentile / 100)
188
+ cond = lambda x: (x >= thershold).sum()
189
+
190
+ elif isinstance(percentile, list) and len(percentile) == 1:
191
+ # Extract the single element from the list
192
+ single_percentile = percentile[0]
193
+ bottom_text = rf'$CPF:\ >{int(single_percentile)}^{{th}}$'
194
+ threshold = df[val].quantile(single_percentile / 100)
195
+ cond = lambda x: (x >= threshold).sum()
196
+
197
+ else:
198
+ bottom_text = rf'$CPF:\ {int(percentile[0])}^{{th}}\ to\ {int(percentile[1])}^{{th}}$'
199
+ thershold_small, thershold_large = df[val].quantile([percentile[0] / 100, percentile[1] / 100])
200
+ cond = lambda x: ((x >= thershold_small) & (x < thershold_large)).sum()
201
+
202
+ histogram = (grouped[val].apply(cond) / grouped[val].count()).unstack().values.T
203
+
204
+ # if np.isnan(histogram).all():
205
+ # raise "CBPF_array contains only NaN values."
206
+ # else:
207
+ # print(f"\nHistogram contains NaN before masking: {np.isnan(histogram).sum()}")
208
+
209
+ histogram_filled = np.nan_to_num(histogram, nan=0) # 將 NaN 替換為 0
210
+
211
+ filtered_histogram = gaussian_filter(histogram_filled, sigma=sigma)
212
+ filtered_histogram[np.isnan(histogram)] = np.nan
213
+
214
+ def is_within_circle(center_row, center_col, row, col, radius):
215
+ return np.sqrt((center_row - row) ** 2 + (center_col - col) ** 2) <= radius
216
+
217
+ def remove_lonely_point(filtered_histogram, radius=3):
218
+ rows, cols = filtered_histogram.shape
219
+ data_positions = np.where(~np.isnan(filtered_histogram))
220
+
221
+ for row, col in zip(*data_positions):
222
+ valid_data_count = 0
223
+ for i in range(max(0, row - radius), min(rows, row + radius + 1)):
224
+ for j in range(max(0, col - radius), min(cols, col + radius + 1)):
225
+ if (i, j) != (row, col) and is_within_circle(row, col, i, j, radius):
226
+ if not np.isnan(filtered_histogram[i, j]):
227
+ valid_data_count += 1
228
+
229
+ if valid_data_count <= 13:
230
+ filtered_histogram[row, col] = np.nan
231
+
232
+ return filtered_histogram
233
+
234
+ def fill_nan_with_mean(filtered_histogram, radius=3):
235
+ rows, cols = filtered_histogram.shape
236
+ nan_positions = np.where(np.isnan(filtered_histogram))
237
+
238
+ for row, col in zip(*nan_positions):
239
+ surrounding_values = []
240
+ surrounding_values_within_one = []
241
+ nan_count = 0
242
+
243
+ for i in range(max(0, row - radius), min(rows, row + radius + 1)):
244
+ for j in range(max(0, col - radius), min(cols, col + radius + 1)):
245
+ if (i, j) != (row, col) and is_within_circle(row, col, i, j, radius):
246
+ if np.isnan(filtered_histogram[i, j]):
247
+ nan_count += 1
248
+ else:
249
+ surrounding_values.append(filtered_histogram[i, j])
250
+
251
+ for i in range(max(0, row - 2), min(rows, row + 2 + 1)):
252
+ for j in range(max(0, col - 2), min(cols, col + 2 + 1)):
253
+ if (i, j) != (row, col) and is_within_circle(row, col, i, j, 2):
254
+ if np.isnan(filtered_histogram[i, j]):
255
+ pass
256
+ else:
257
+ surrounding_values_within_one.append(filtered_histogram[i, j])
258
+
259
+ if nan_count < 13 and surrounding_values_within_one:
260
+ filtered_histogram[row, col] = np.mean(surrounding_values)
261
+
262
+ return filtered_histogram
263
+
264
+ # Apply the function to your data
265
+ filtered_histogram = remove_lonely_point(filtered_histogram)
266
+ filtered_histogram = fill_nan_with_mean(filtered_histogram)
267
+
268
+ # plot
269
+ fig, ax = plt.subplots()
270
+ fig.subplots_adjust(left=0)
271
+
272
+ surf = ax.pcolormesh(X, Y, filtered_histogram, shading='auto', cmap='jet', antialiased=True)
273
+
274
+ max_ws = max_ws or np.concatenate((abs(df.u), abs(df.v))).max() # Get the maximum value of the wind speed
275
+
276
+ radius_lst = np.arange(1, math.ceil(max_ws) + 1) # Create a list of radius
277
+
278
+ for i, radius in enumerate(radius_lst):
279
+ circle = plt.Circle((0, 0), radius, fill=False, color='gray', linewidth=1, linestyle='--', alpha=0.5)
280
+ ax.add_artist(circle)
281
+
282
+ for angle, label in zip(range(0, 360, 90), ["E", "N", "W", "S"]):
283
+ radian = np.radians(angle)
284
+ line_x, line_y = radius * np.cos(radian), radius * np.sin(radian)
285
+
286
+ if i + 2 == len(radius_lst): # Add wind direction line and direction label at the edge of the circle
287
+ ax.plot([0, line_x * 1.05], [0, line_y * 1.05], color='k', linestyle='-', linewidth=1, alpha=0.5)
288
+ ax.text(line_x * 1.15, line_y * 1.15, label, ha='center', va='center')
289
+
290
+ ax.text(radius * np.cos(np.radians(rlabel_pos)), radius * np.sin(np.radians(rlabel_pos)),
291
+ str(radius) + ' m/s', ha='center', va='center', fontsize=8)
292
+
293
+ for radius in range(math.ceil(max_ws) + 1, 10):
294
+ circle = plt.Circle((0, 0), radius, fill=False, color='gray', linewidth=1, linestyle='--', alpha=0.5)
295
+ ax.add_artist(circle)
296
+
297
+ ax.set(xlim=(-max_ws * 1.02, max_ws * 1.02),
298
+ ylim=(-max_ws * 1.02, max_ws * 1.02),
299
+ xticks=[],
300
+ yticks=[],
301
+ xticklabels=[],
302
+ yticklabels=[],
303
+ aspect='equal',
304
+ )
305
+ if bottom_text:
306
+ ax.text(0.50, -0.05, bottom_text, fontweight='bold', fontsize=8, va='center', ha='center',
307
+ transform=ax.transAxes)
308
+
309
+ ax.text(0.5, 1.05, Unit(val), fontweight='bold', fontsize=12, va='center', ha='center', transform=ax.transAxes)
310
+
311
+ cbar = plt.colorbar(surf, ax=ax, label='Frequency', pad=0.01, fraction=0.04)
312
+ cbar.ax.yaxis.label.set_fontsize(8)
313
+ cbar.ax.tick_params(labelsize=8)
314
+
315
+ plt.show()
316
+
317
+ return fig, ax
@@ -0,0 +1,2 @@
1
+ from .aethalometer import *
2
+ from .optical import *
@@ -0,0 +1,77 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ from pandas import date_range
4
+
5
+ from AeroViz.plot.utils import *
6
+
7
+ __all__ = ['plot_MA350',
8
+ 'plot_MA3502',
9
+ 'plot_day_night']
10
+
11
+
12
+ @set_figure(figsize=(15, 5))
13
+ def plot_MA350(df, **kwargs):
14
+ fig, ax = plt.subplots()
15
+
16
+ # ax.scatter(df.index, df['UV BCc'], marker='o', c='purple', alpha=0.5, label='UV BCc')
17
+ # ax.scatter(df.index, df['Blue BCc'], c='b', alpha=0.5, label='Blue BCc')
18
+ # ax.scatter(df.index, df['Green BCc'], c='g', alpha=0.5, label='Green BCc')
19
+ # ax.scatter(df.index, df['Red BCc'], c='r', alpha=0.5, label='Red BCc')
20
+ mean, std = round(df.mean(), 2), round(df.std(), 2)
21
+
22
+ label1 = rf'$MA350-0171\ :\;{mean["MA350_0171 IR BCc"]}\;\pm\;{std["MA350_0171 IR BCc"]}\;(ng/m^3)$'
23
+ label2 = rf'$MA350-0176\ :\;{mean["MA350_0176 IR BCc"]}\;\pm\;{std["MA350_0176 IR BCc"]}\;(ng/m^3)$'
24
+ label3 = rf'$BC-1054\ :\;{mean["BC1054 IR BCc"]}\;\pm\;{std["BC1054 IR BCc"]}\;(ng/m^3)$'
25
+ ax.scatter(df.index, df['MA350_0171 IR BCc'], s=10, ls='-', marker='o', c='#a3b18a', alpha=0.5, label=label1)
26
+ ax.scatter(df.index, df['MA350_0176 IR BCc'], s=10, ls='-', marker='o', c='#3a5a40', alpha=0.5, label=label2)
27
+ ax.scatter(df.index, df['BC1054 IR BCc'], s=10, ls='-', marker='o', c='g', alpha=0.5, label=label3)
28
+ ax.legend(prop={'weight': 'bold'}, loc='upper left')
29
+
30
+ st_tm, fn_tm = df.index[0], df.index[-1]
31
+ tick_time = date_range(st_tm, fn_tm, freq=kwargs.get('freq', '10d'))
32
+
33
+ ax.set(xlabel=kwargs.get('xlabel', ''),
34
+ ylabel=kwargs.get('ylabel', r'$BC\ (ng/m^3)$'),
35
+ xticks=kwargs.get('xticks', tick_time),
36
+ xticklabels=kwargs.get('xticklabels', [_tm.strftime("%F") for _tm in tick_time]),
37
+ xlim=kwargs.get('xlim', (st_tm, fn_tm)),
38
+ ylim=kwargs.get('ylim', (0, None)),
39
+ )
40
+
41
+
42
+ @set_figure
43
+ def plot_MA3502(df):
44
+ fig, ax = plt.subplots()
45
+
46
+ bins = np.array([375, 470, 528, 625, 880])
47
+ vals = df.dropna().iloc[:, -5:].values
48
+
49
+ ax.boxplot(vals, positions=bins, widths=20,
50
+ showfliers=False, showmeans=True, meanline=True, patch_artist=True,
51
+ boxprops=dict(facecolor='#f2c872', alpha=.7),
52
+ meanprops=dict(color='#000000', ls='none'),
53
+ medianprops=dict(ls='-', color='#000000'))
54
+
55
+ ax.set(xlim=(355, 900),
56
+ ylim=(0, None),
57
+ xlabel=r'$\lambda\ (nm)$',
58
+ ylabel=r'$Absorption\ (1/Mm)$', )
59
+
60
+
61
+ @set_figure(figsize=(6, 5))
62
+ def plot_day_night(df):
63
+ # Group by hour of day and calculate mean
64
+ df_grouped = df.groupby(df.index.hour).mean()
65
+
66
+ # Create figure and plot
67
+ fig, ax = plt.subplots()
68
+ ax.plot(df_grouped.index, df_grouped['MA350_0171 IR BCc'], marker='o', c='k', alpha=0.5, label='MA350-0171')
69
+ ax.plot(df_grouped.index, df_grouped['MA350_0176 IR BCc'], marker='o', c='r', alpha=0.5, label='MA350-0176')
70
+ ax.plot(df_grouped.index, df_grouped['BC1054 IR BCc'], marker='o', c='b', alpha=0.5, label='BC-1054')
71
+
72
+ ax.set(xlim=(0, 23),
73
+ xlabel='Hour of Day',
74
+ ylabel=r'$BC\ (ng/m^3)$',
75
+ title=f'Diurnal pattern', )
76
+
77
+ ax.legend()