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,92 @@
1
+ from pandas import DatetimeIndex, DataFrame, concat
2
+ from pathlib import Path
3
+ import pickle as pkl
4
+ from datetime import datetime as dtm
5
+
6
+
7
+ class _writter:
8
+
9
+ def __init__(self, path_out=None, excel=True, csv=False):
10
+
11
+ self.path_out = Path(path_out) if path_out is not None else path_out
12
+ self.excel = excel
13
+ self.csv = csv
14
+
15
+ def _pre_process(self, _out):
16
+
17
+ if type(_out) == dict:
18
+ for _ky, _df in _out.items():
19
+ _df.index.name = 'time'
20
+ else:
21
+ _out.index.name = 'time'
22
+
23
+ return _out
24
+
25
+ def _save_out(self, _nam, _out):
26
+
27
+ _check = True
28
+ while _check:
29
+
30
+ try:
31
+ if self.path_out is not None:
32
+ self.path_out.mkdir(exist_ok=True, parents=True)
33
+ with (self.path_out / f'{_nam}.pkl').open('wb') as f:
34
+ pkl.dump(_out, f, protocol=pkl.HIGHEST_PROTOCOL)
35
+
36
+ if self.excel:
37
+ from pandas import ExcelWriter
38
+ with ExcelWriter(self.path_out / f'{_nam}.xlsx') as f:
39
+ if type(_out) == dict:
40
+ for _key, _val in _out.items():
41
+ _val.to_excel(f, sheet_name=f'{_key}')
42
+ else:
43
+ _out.to_excel(f, sheet_name=f'{_nam}')
44
+
45
+ if self.csv:
46
+ if type(_out) == dict:
47
+ _path_out = self.path_out / _nam
48
+ _path_out.mkdir(exist_ok=True, parents=True)
49
+
50
+ for _key, _val in _out.items():
51
+ _val.to_csv(_path_out / f'{_key}.csv')
52
+ else:
53
+ _out.to_csv(self.path_out / f'{_nam}.csv')
54
+
55
+ _check = False
56
+
57
+ except PermissionError as _err:
58
+ print('\n', _err)
59
+ input('\t\t\33[41m Please Close The File And Press "Enter" \33[0m\n')
60
+
61
+
62
+ def _run_process(*_ini_set):
63
+ def _decorator(_prcs_fc):
64
+ def _wrap(*arg, **kwarg):
65
+ _fc_name, _nam = _ini_set
66
+
67
+ if kwarg.get('nam') is not None:
68
+ _nam = kwarg.pop('nam')
69
+
70
+ print(f"\n\t{dtm.now().strftime('%m/%d %X')} : Process \033[92m{_fc_name}\033[0m -> {_nam}")
71
+
72
+ _class, _out = _prcs_fc(*arg, **kwarg)
73
+ _out = _class._pre_process(_out)
74
+
75
+ _class._save_out(_nam, _out)
76
+
77
+ return _out
78
+
79
+ return _wrap
80
+
81
+ return _decorator
82
+
83
+
84
+ def _union_index(*_df_arg):
85
+ _idx = concat(_df_arg, axis=1).index
86
+
87
+ # _idx = DatetimeIndex([])
88
+
89
+ # for _df in _df_arg:
90
+ # _idx = _idx.union(DataFrame(_df).index)
91
+
92
+ return [_df.reindex(_idx) if _df is not None else None for _df in _df_arg]
@@ -0,0 +1,7 @@
1
+ from . import distribution
2
+ from . import improve
3
+ from . import meteorology
4
+ from . import optical
5
+ from . import timeseries
6
+ from .templates import *
7
+ from .utils import *
@@ -0,0 +1 @@
1
+ from .distribution import *
@@ -0,0 +1,582 @@
1
+ from typing import Literal
2
+
3
+ import matplotlib.colors as colors
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ from matplotlib.collections import PolyCollection
7
+ from matplotlib.pyplot import Figure, Axes
8
+ from matplotlib.ticker import FuncFormatter
9
+ from numpy import log, exp, sqrt, pi
10
+ from pandas import DataFrame, Series, date_range
11
+ from scipy.optimize import curve_fit
12
+ from scipy.signal import find_peaks
13
+ from scipy.stats import norm, lognorm
14
+ from tabulate import tabulate
15
+
16
+ from AeroViz.plot.utils import *
17
+
18
+ __all__ = [
19
+ 'plot_dist',
20
+ 'heatmap',
21
+ 'heatmap_tms',
22
+ 'three_dimension',
23
+ 'curve_fitting'
24
+ ]
25
+
26
+
27
+ @set_figure
28
+ def plot_dist(data: DataFrame | np.ndarray,
29
+ data_std: DataFrame | None = None,
30
+ std_scale: float | None = 1,
31
+ unit: Literal["Number", "Surface", "Volume", "Extinction"] = 'Number',
32
+ additional: Literal["Std", "Enhancement", "Error"] = None,
33
+ fig: Figure | None = None,
34
+ ax: Axes | None = None,
35
+ **kwargs
36
+ ) -> tuple[Figure, Axes]:
37
+ """
38
+ Plot particle size distribution curves and optionally show enhancements.
39
+
40
+ Parameters
41
+ ----------
42
+ data : dict or list
43
+ If dict, keys are labels and values are arrays of distribution values.
44
+ If listed, it should contain three arrays for different curves.
45
+ data_std : dict
46
+ Dictionary containing standard deviation data for ambient extinction distribution.
47
+ std_scale : float
48
+ The width of standard deviation.
49
+ unit : {'Number', 'Surface', 'Volume', 'Extinction'}
50
+ Unit of measurement for the data.
51
+ additional : {'std', 'enhancement', 'error'}
52
+ Whether to show enhancement curves.
53
+ fig : Figure, optional
54
+ Matplotlib Figure object to use.
55
+ ax : AxesSubplot, optional
56
+ Matplotlib AxesSubplot object to use. If not provided, a new subplot will be created.
57
+ **kwargs : dict
58
+ Additional keyword arguments.
59
+
60
+ Returns
61
+ -------
62
+ ax : AxesSubplot
63
+ Matplotlib AxesSubplot.
64
+
65
+ Examples
66
+ --------
67
+ >>> plot_dist(DataFrame(...), additional="Enhancement")
68
+ """
69
+ fig, ax = plt.subplots(**{**{'figsize': (6, 2)}, **kwargs.get('fig_kws', {})}) if ax is None else (
70
+ ax.get_figure(), ax)
71
+
72
+ # plot_kws
73
+ plot_kws = dict(ls='solid', lw=2, alpha=0.8, **kwargs.get('plot_kws', {}))
74
+
75
+ # Receive input data
76
+ dp = np.array(data.columns, dtype=float)
77
+ states = np.array(data.index)
78
+
79
+ for state in states:
80
+ mean = data.loc[state].to_numpy()
81
+ ax.plot(dp, mean, label=state, color=Color.color_choose[state][0], **plot_kws)
82
+
83
+ if additional == 'Std':
84
+ std = data_std.loc[state].to_numpy() * std_scale
85
+ ax.fill_between(dp, y1=mean - std, y2=mean + std, alpha=0.4, color=Color.color_choose[state][1],
86
+ edgecolor=None, label='__nolegend__')
87
+
88
+ # figure_set
89
+ ax.set(xlim=(dp.min(), dp.max()), ylim=(0, None), xscale='log',
90
+ xlabel=r'$D_{p} (nm)$', ylabel=Unit(f'{unit}_dist'), title=kwargs.get('title', unit))
91
+
92
+ ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 3), useMathText=True)
93
+ ax.grid(axis='x', which='major', color='k', linestyle='dashdot', linewidth=0.4, alpha=0.4)
94
+
95
+ Clean = data.loc['Clean'].to_numpy()
96
+ Transition = data.loc['Transition'].to_numpy()
97
+ Event = data.loc['Event'].to_numpy()
98
+
99
+ if additional == "Enhancement":
100
+ ax2 = ax.twinx()
101
+ ax2.plot(dp, Transition / Clean, ls='dashed', color='k', label=f'{additional} ratio 1')
102
+ ax2.plot(dp, Event / Transition, ls='dashed', color='gray', label=f'{additional} ratio 2')
103
+ ax2.set(ylabel='Enhancement ratio')
104
+
105
+ elif additional == "Error":
106
+ ax2 = ax.twinx()
107
+ error1 = np.where(Transition != 0, np.abs(Clean - Transition) / Clean * 100, 0)
108
+ error2 = np.where(Event != 0, np.abs(Transition - Event) / Transition * 100, 0)
109
+
110
+ ax2.plot(dp, error1, ls='--', color='k', label='Error 1 ')
111
+ ax2.plot(dp, error2, ls='--', color='gray', label='Error 2')
112
+ ax2.set(ylabel='Error (%)')
113
+
114
+ # Combine legends from ax and ax2
115
+ axes_list = fig.get_axes()
116
+ legends_combined = [legend for axes in axes_list for legend in axes.get_legend_handles_labels()[0]]
117
+ labels_combined = [label for axes in axes_list for label in axes.get_legend_handles_labels()[1]]
118
+
119
+ ax.legend(legends_combined, labels_combined, prop={'weight': 'bold'})
120
+
121
+ plt.show()
122
+
123
+ return fig, ax
124
+
125
+
126
+ @set_figure
127
+ def heatmap(data: DataFrame,
128
+ unit: Literal["Number", "Surface", "Volume", "Extinction"],
129
+ cmap: str = 'Blues',
130
+ colorbar: bool = False,
131
+ magic_number: int = 11,
132
+ ax: Axes | None = None,
133
+ **kwargs
134
+ ) -> tuple[Figure, Axes]:
135
+ """
136
+ Plot a heatmap of particle size distribution.
137
+
138
+ Parameters
139
+ ----------
140
+ data : pandas.DataFrame
141
+ The data containing particle size distribution values. Each column corresponds to a size bin,
142
+ and each row corresponds to a different distribution.
143
+
144
+ unit : {'Number', 'Surface', 'Volume', 'Extinction'}, optional
145
+ The unit of measurement for the data.
146
+
147
+ cmap : str, default='Blues'
148
+ The colormap to use for the heatmap.
149
+
150
+ colorbar : bool, default=False
151
+ Whether to show the colorbar.
152
+
153
+ magic_number : int, default=11
154
+ The number of bins to use for the histogram.
155
+
156
+ ax : matplotlib.axes.Axes, optional
157
+ The axes to plot the heatmap on. If not provided, a new subplot will be created.
158
+
159
+ **kwargs
160
+ Additional keyword arguments to pass to matplotlib functions.
161
+
162
+ Returns
163
+ -------
164
+ matplotlib.axes.Axes
165
+ The Axes object containing the heatmap.
166
+
167
+ Examples
168
+ --------
169
+ >>> heatmap(DataFrame(...), unit='Number')
170
+
171
+ Notes
172
+ -----
173
+ This function calculates a 2D histogram of the log-transformed particle sizes and the distribution values.
174
+ It then plots the heatmap using a logarithmic color scale.
175
+
176
+ """
177
+ fig, ax = plt.subplots(**{**{'figsize': (3, 3)}, **kwargs.get('fig_kws', {})}) if ax is None else (
178
+ ax.get_figure(), ax)
179
+
180
+ min_value = 1e-8
181
+ dp = np.array(data.columns, dtype=float)
182
+ x = np.append(np.tile(dp, data.to_numpy().shape[0]), np.log(dp).max())
183
+ y = np.append(data.to_numpy().flatten(), min_value)
184
+
185
+ # mask NaN
186
+ x = x[~np.isnan(y)]
187
+ y = y[~np.isnan(y)]
188
+
189
+ # using log(x)
190
+ histogram, xedges, yedges = np.histogram2d(np.log(x), y, bins=len(dp) + magic_number)
191
+ histogram[histogram == 0] = min_value # Avoid log(0)
192
+
193
+ plot_kws = dict(norm=colors.LogNorm(vmin=1, vmax=histogram.max()), cmap=cmap, **kwargs.get('plot_kws', {}))
194
+
195
+ pco = ax.pcolormesh(xedges[:-1], yedges[:-1], histogram.T, shading='gouraud', **plot_kws)
196
+
197
+ # TODO:
198
+ ax.plot(np.log(dp), data.mean() + data.std(), ls='dashed', color='r', label='pollutant')
199
+ ax.plot(np.log(dp), data.mean(), ls='dashed', color='k', alpha=0.5, label='mean')
200
+ ax.plot(np.log(dp), data.mean() - data.std(), ls='dashed', color='b', label='clean')
201
+
202
+ ax.set(xlim=(np.log(dp).min(), np.log(dp).max()), ylim=(0, None),
203
+ xlabel=r'$D_{p} (nm)$', ylabel=Unit(f'{unit}_dist'), title=kwargs.get('title', unit))
204
+
205
+ major_ticks = np.power(10, np.arange(np.ceil(np.log10(dp.min())), np.floor(np.log10(dp.max())) + 1))
206
+ minor_ticks = [v for v in np.concatenate([_ * np.arange(2, 10) for _ in major_ticks]) if min(dp) <= v <= max(dp)]
207
+
208
+ ax.set_xticks(np.log(major_ticks))
209
+ ax.set_xticks(np.log(minor_ticks), minor=True)
210
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda tick, pos: "{:.0f}".format(np.exp(tick))))
211
+
212
+ ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 3), useMathText=True)
213
+ ax.grid(axis='x', which='major', color='k', linestyle='dashdot', linewidth=0.4, alpha=0.4)
214
+ ax.legend(prop={'weight': 'bold'})
215
+
216
+ if colorbar:
217
+ plt.colorbar(pco, pad=0.02, fraction=0.05, label='Counts', **kwargs.get('cbar_kws', {}))
218
+
219
+ plt.show()
220
+
221
+ return fig, ax
222
+
223
+
224
+ @set_figure
225
+ def heatmap_tms(data: DataFrame,
226
+ unit: Literal["Number", "Surface", "Volume", "Extinction"],
227
+ cmap: str = 'jet',
228
+ ax: Axes | None = None,
229
+ **kwargs
230
+ ) -> tuple[Figure, Axes]:
231
+ """ Plot the size distribution over time.
232
+
233
+ Parameters
234
+ ----------
235
+ data : DataFrame
236
+ A DataFrame of particle concentrations to plot the heatmap.
237
+
238
+ ax : matplotlib.axis.Axis
239
+ An axis object to plot on. If none is provided, one will be created.
240
+
241
+ unit : Literal["Number", "Surface", "Volume", "Extinction"]
242
+ default='Number'
243
+
244
+ cmap : matplotlib.colormap, default='viridis'
245
+ The colormap to use. Can be anything other that 'jet'.
246
+
247
+ Returns
248
+ -------
249
+ ax : matplotlib.axis.Axis
250
+
251
+ Notes
252
+ -----
253
+ Do not dropna when using this code.
254
+
255
+ Examples
256
+ --------
257
+ Plot a SPMS + APS data:
258
+ >>> heatmap_tms(DataFrame(...), cmap='jet')
259
+ """
260
+ fig, ax = plt.subplots(
261
+ **{**{'figsize': (len(data.index) * 0.01, 2)}, **kwargs.get('fig_kws', {})}) if ax is None else (
262
+ ax.get_figure(), ax)
263
+
264
+ time = data.index
265
+ dp = np.array(data.columns, dtype=float)
266
+
267
+ # data = data.interpolate(method='linear', axis=0)
268
+ data = np.nan_to_num(data.to_numpy())
269
+
270
+ vmin_mapping = {'Number': 1e2, 'Surface': 1e8, 'Volume': 1e9, 'Extinction': 1}
271
+
272
+ # Set the colorbar min and max based on the min and max of the values
273
+ cbar_min = kwargs.get('cbar_kws', {}).pop('cbar_min', vmin_mapping[unit])
274
+ cbar_max = kwargs.get('cbar_kws', {}).pop('cbar_max', np.nanmax(data))
275
+
276
+ # Set the plot_kws
277
+ plot_kws = dict(norm=colors.LogNorm(vmin=cbar_min, vmax=cbar_max), cmap=cmap, **kwargs.get('plot_kws', {}))
278
+
279
+ # main plot
280
+ pco = ax.pcolormesh(time, dp, data.T, shading='auto', **plot_kws)
281
+
282
+ # Set ax
283
+ st_tm, fn_tm = time[0], time[-1]
284
+ tick_time = date_range(st_tm, fn_tm, freq=kwargs.get('freq', '10d')).strftime("%F")
285
+
286
+ ax.set(xlim=(st_tm, fn_tm),
287
+ ylim=(dp.min(), dp.max()),
288
+ ylabel='$D_p (nm)$',
289
+ xticks=tick_time,
290
+ xticklabels=tick_time,
291
+ yscale='log',
292
+ title=kwargs.get('title', f'{st_tm.strftime("%F")} - {fn_tm.strftime("%F")}'))
293
+
294
+ plt.colorbar(pco, pad=0.02, fraction=0.02, label=Unit(f'{unit}_dist'), **kwargs.get('cbar_kws', {}))
295
+
296
+ plt.show()
297
+
298
+ return fig, ax
299
+
300
+
301
+ @set_figure
302
+ def three_dimension(data: DataFrame | np.ndarray,
303
+ unit: Literal["Number", "Surface", "Volume", "Extinction"],
304
+ cmap: str = 'Blues',
305
+ ax: Axes | None = None,
306
+ **kwargs
307
+ ) -> tuple[Figure, Axes]:
308
+ """
309
+ Create a 3D plot with data from a pandas DataFrame or numpy array.
310
+
311
+ Parameters
312
+ ----------
313
+ data : DataFrame or ndarray
314
+ Input data containing the values to be plotted.
315
+
316
+ unit : {'Number', 'Surface', 'Volume', 'Extinction'}
317
+ Unit of measurement for the data.
318
+
319
+ cmap : str, default='Blues'
320
+ The colormap to use for the facecolors.
321
+
322
+ ax : AxesSubplot, optional
323
+ Matplotlib AxesSubplot. If not provided, a new subplot will be created.
324
+ **kwargs
325
+ Additional keyword arguments to customize the plot.
326
+
327
+ Returns
328
+ -------
329
+ Axes
330
+ Matplotlib Axes object representing the 3D plot.
331
+
332
+ Notes
333
+ -----
334
+ - The function creates a 3D plot with data provided in a pandas DataFrame or numpy array.
335
+ - The x-axis is logarithmically scaled, and ticks and labels are formatted accordingly.
336
+ - Additional customization can be done using the **kwargs.
337
+
338
+ Example
339
+ -------
340
+ >>> three_dimension(DataFrame(...), unit='Number', cmap='Blues')
341
+ """
342
+ fig, ax = plt.subplots(figsize=(4, 4), subplot_kw={"projection": "3d"},
343
+ **kwargs.get('fig_kws', {})) if ax is None else (ax.get_figure(), ax)
344
+
345
+ dp = np.array(['11.7', *data.columns, '2437.4'], dtype=float)
346
+ lines = data.shape[0]
347
+
348
+ _X, _Y = np.meshgrid(np.log(dp), np.arange(lines))
349
+ _Z = np.pad(data, ((0, 0), (1, 1)), 'constant')
350
+
351
+ verts = []
352
+ for i in range(_X.shape[0]):
353
+ verts.append(list(zip(_X[i, :], _Z[i, :])))
354
+
355
+ facecolors = plt.colormaps[cmap](np.linspace(0, 1, len(verts)))
356
+ poly = PolyCollection(verts, facecolors=facecolors, edgecolors='k', lw=0.5, alpha=.7)
357
+ ax.add_collection3d(poly, zs=range(1, lines + 1), zdir='y')
358
+
359
+ ax.set(xlim=(np.log(11.7), np.log(2437.4)), ylim=(1, lines), zlim=(0, np.nanmax(_Z)),
360
+ xlabel='$D_{p} (nm)$', ylabel='Class', zlabel=Unit(f'{unit}_dist'))
361
+
362
+ ax.set_xticks(np.log([10, 100, 1000]))
363
+ ax.set_xticks(np.log([20, 30, 40, 50, 60, 70, 80, 90, 200, 300, 400, 500, 600, 700, 800, 900, 2000]), minor=True)
364
+ ax.xaxis.set_major_formatter(FuncFormatter((lambda tick, pos: "{:.0f}".format(np.exp(tick)))))
365
+ ax.ticklabel_format(axis='z', style='sci', scilimits=(0, 3), useMathText=True)
366
+
367
+ ax.zaxis.get_offset_text().set_visible(False)
368
+ exponent = np.floor(np.log10(np.nanmax(data))).astype(int)
369
+ ax.text(ax.get_xlim()[1] * 1.05, ax.get_ylim()[1], ax.get_zlim()[1] * 1.1, s=fr'${{\times}}\ 10^{exponent}$')
370
+
371
+ plt.show()
372
+
373
+ return fig, ax
374
+
375
+
376
+ @set_figure
377
+ def curve_fitting(dp: np.ndarray,
378
+ dist: np.ndarray | Series | DataFrame,
379
+ mode: int = None,
380
+ unit: Literal["Number", "Surface", "Volume", "Extinction"] = None,
381
+ ax: Axes | None = None,
382
+ **kwargs
383
+ ) -> tuple[Figure, Axes]:
384
+ """
385
+ Fit a log-normal distribution to the given data and plot the result.
386
+
387
+ Parameters
388
+ ----------
389
+ - dp (array): Array of diameter values.
390
+ - dist (array): Array of distribution values corresponding to each diameter.
391
+ - mode (int, optional): Number of log-normal distribution to fit (default is None).
392
+ - **kwargs: Additional keyword arguments to be passed to the plot_function.
393
+
394
+ Returns
395
+ -------
396
+ None
397
+
398
+ Notes
399
+ -----
400
+ - The function fits a sum of log-normal distribution to the input data.
401
+ - The number of distribution is determined by the 'mode' parameter.
402
+ - Additional plotting customization can be done using the **kwargs.
403
+
404
+ Example
405
+ -------
406
+ >>> curve_fitting(dp, dist, mode=2, xlabel="Diameter (nm)", ylabel="Distribution")
407
+ """
408
+ fig, ax = plt.subplots(**kwargs.get('fig_kws', {})) if ax is None else (ax.get_figure(), ax)
409
+
410
+ # Calculate total number concentration and normalize distribution
411
+ total_num = np.sum(dist * log(dp))
412
+ norm_data = dist / total_num
413
+
414
+ def lognorm_func(x, *params):
415
+ num_distributions = len(params) // 3
416
+ result = np.zeros_like(x)
417
+
418
+ for i in range(num_distributions):
419
+ offset = i * 3
420
+ _number, _geomean, _geostd = params[offset: offset + 3]
421
+
422
+ result += (_number / (log(_geostd) * sqrt(2 * pi)) *
423
+ exp(-(log(x) - log(_geomean)) ** 2 / (2 * log(_geostd) ** 2)))
424
+
425
+ return result
426
+
427
+ # initial gauss
428
+ min_value = np.array([min(dist)])
429
+ extend_ser = np.concatenate([min_value, dist, min_value])
430
+ _mode, _ = find_peaks(extend_ser, distance=20)
431
+ peak = dp[_mode - 1]
432
+ mode = mode or len(peak)
433
+
434
+ # 初始參數猜測
435
+ initial_guess = [0.05, 20., 2.] * mode
436
+
437
+ # 設定參數範圍
438
+ bounds = ([1e-6, 10, 1] * mode, [1, 3000, 8] * mode)
439
+
440
+ # 使用 curve_fit 函數進行擬合
441
+ result = curve_fit(lognorm_func, dp, norm_data, p0=initial_guess, bounds=bounds)
442
+
443
+ # 獲取擬合的參數
444
+ params = result[0].tolist()
445
+
446
+ print('\n' + "Fitting Results:")
447
+ table = []
448
+
449
+ for i in range(mode):
450
+ offset = i * 3
451
+ num, mu, sigma = params[offset:offset + 3]
452
+ table.append([f'log-{i + 1}', f"{num * total_num:.3f}", f"{mu:.3f}", f"{sigma:.3f}"])
453
+
454
+ # 使用 tabulate 來建立表格並印出
455
+ print(tabulate(table, headers=["log-", "number", "mu", "sigma"], floatfmt=".3f", tablefmt="fancy_grid"))
456
+
457
+ fit_curve = total_num * lognorm_func(dp, *params)
458
+
459
+ plt.plot(dp, fit_curve, color='#c41b1b', label='Fitting curve', lw=2.5)
460
+ plt.plot(dp, dist, color='b', label='Observed curve', lw=2.5)
461
+
462
+ ax.set(xlim=(dp.min(), dp.max()), ylim=(0, None), xscale='log',
463
+ xlabel=r'$\bf D_{p}\ (nm)$', ylabel=Unit(f'{unit}_dist'), title=kwargs.get('title'))
464
+
465
+ plt.grid(color='k', axis='x', which='major', linestyle='dashdot', linewidth=0.4, alpha=0.4)
466
+ ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 3), useMathText=True)
467
+ ax.legend(prop={'weight': 'bold'})
468
+
469
+ plt.show(block=True)
470
+
471
+ return fig, ax
472
+
473
+
474
+ @set_figure
475
+ def ls_mode(**kwargs) -> tuple[Figure, Axes]:
476
+ """
477
+ Plot log-normal mass size distribution for small mode, large mode, and sea salt particles.
478
+
479
+ Parameters
480
+ ----------
481
+ **kwargs : dict
482
+ Additional keyword arguments.
483
+
484
+ Examples
485
+ --------
486
+ Example : Plot log-normal mass size distribution with default settings
487
+ >>> ls_mode()
488
+ """
489
+
490
+ fig, ax = plt.subplots(**kwargs.get('fig_kws', {}))
491
+
492
+ geoMean = [0.2, 0.5, 2.5]
493
+ geoStdv = [2.2, 1.5, 2.0]
494
+ color = ['g', 'r', 'b']
495
+ label = [r'$\bf Small\ mode\ :D_{g}\ =\ 0.2\ \mu m,\ \sigma_{{g}}\ =\ 2.2$',
496
+ r'$\bf Large\ mode\ :D_{g}\ =\ 0.5\ \mu m,\ \sigma_{{g}}\ =\ 1.5$',
497
+ r'$\bf Sea\ salt\ :D_{g}\ =\ 2.5\ \mu m,\ \sigma_{{g}}\ =\ 2.0$']
498
+
499
+ x = np.geomspace(0.001, 20, 10000)
500
+ for _gmd, _gsd, _color, _label in zip(geoMean, geoStdv, color, label):
501
+ lognorm = 1 / (log(_gsd) * sqrt(2 * pi)) * (exp(-(log(x) - log(_gmd)) ** 2 / (2 * log(_gsd) ** 2)))
502
+
503
+ ax.semilogx(x, lognorm, color=_color, label=_label)
504
+ ax.fill_between(x, lognorm, 0, where=(lognorm > 0), color=_color, alpha=0.3, label='__nolegend__')
505
+
506
+ ax.set(xlim=(0.001, 20), ylim=(0, None), xscale='log', xlabel=r'$\bf D_{p}\ (nm)$',
507
+ ylabel=r'$\bf Probability\ (dM/dlogdp)$', title=r'Log-normal Mass Size Distribution')
508
+
509
+ ax.grid(color='k', axis='x', which='major', linestyle='dashdot', linewidth=0.4, alpha=0.4)
510
+ ax.legend(prop={'weight': 'bold'})
511
+
512
+ plt.show()
513
+
514
+ return fig, ax
515
+
516
+
517
+ @set_figure
518
+ def lognorm_dist(**kwargs) -> tuple[Figure, Axes]:
519
+ #
520
+ """
521
+ Plot various particle size distribution to illustrate log-normal distribution and transformations.
522
+
523
+ Parameters
524
+ ----------
525
+ **kwargs : dict
526
+ Additional keyword arguments.
527
+
528
+ Examples
529
+ --------
530
+ Example : Plot default particle size distribution
531
+ >>> lognorm_dist()
532
+ """
533
+
534
+ fig, ax = plt.subplots(2, 2, **kwargs.get('fig_kws', {}))
535
+ ([ax1, ax2], [ax3, ax4]) = ax
536
+ fig.suptitle('Particle Size Distribution', fontweight='bold')
537
+ plt.subplots_adjust(left=0.125, right=0.925, bottom=0.1, top=0.93, wspace=0.4, hspace=0.4)
538
+
539
+ # pdf
540
+ normpdf = lambda x, mu, sigma: (1 / (sigma * sqrt(2 * pi))) * exp(-(x - mu) ** 2 / (2 * sigma ** 2))
541
+ lognormpdf = lambda x, gmean, gstd: (1 / (log(gstd) * sqrt(2 * pi))) * exp(
542
+ -(log(x) - log(gmean)) ** 2 / (2 * log(gstd) ** 2))
543
+ lognormpdf2 = lambda x, gmean, gstd: (1 / (x * log(gstd) * sqrt(2 * pi))) * exp(
544
+ -(log(x) - log(gmean)) ** 2 / (2 * log(gstd) ** 2))
545
+
546
+ # 生成x
547
+ x = np.linspace(-10, 10, 1000)
548
+ x2 = np.geomspace(0.01, 100, 1000)
549
+
550
+ # Question 1
551
+ # 若對數常態分布x有gmd=3, gstd=2,ln(x) ~ 常態分佈,試問其分布的平均值與標準差?? Y ~ N(mu=log(gmean), sigma=log(gstd))
552
+ data1 = lognorm(scale=3, s=log(2)).rvs(size=5000)
553
+
554
+ # Question 2
555
+ # 若常態分布x有平均值3 標準差1,exp(x)則為一對數常態分佈? 由對數常態分佈的定義 若隨機變數ln(Z)是常態分布 則Z為對數常態分布
556
+ # 因此已知Z = exp(x), so ln(Z)=x,Z ~ 對數常態分佈,試問其分布的幾何平均值與幾何標準差是?? Z ~ LN(geoMean=exp(mu), geoStd=exp(sigma))
557
+ data2 = norm(loc=3, scale=1).rvs(size=5000)
558
+
559
+ def plot_distribution(ax, x, pdf, color='k-', xscale='linear'):
560
+ ax.plot(x, pdf, color)
561
+ ax.set(xlabel='Particle Size (micron)', ylabel='Probability Density', xlim=(x.min(), x.max()), xscale=xscale)
562
+
563
+ # 繪製粒徑分布
564
+ plot_distribution(ax1, x, normpdf(x, mu=0, sigma=2))
565
+
566
+ plot_distribution(ax2, x2, lognormpdf(x2, gmean=0.8, gstd=1.5), 'g-', xscale='log')
567
+ plot_distribution(ax2, x2, lognormpdf2(x2, gmean=0.8, gstd=1.5), 'r--', xscale='log')
568
+ plot_distribution(ax2, x2, lognorm(scale=0.8, s=log(1.5)).pdf(x2), 'b--', xscale='log')
569
+
570
+ plot_distribution(ax3, x, normpdf(x, mu=log(3), sigma=log(2)), 'k-')
571
+ ax3.hist(log(data1), bins=100, density=True, alpha=0.6, color='g')
572
+
573
+ plot_distribution(ax4, x2, lognormpdf2(x2, gmean=exp(3), gstd=exp(1)), 'r-', xscale='log')
574
+ ax4.hist(exp(data2), bins=100, density=True, alpha=0.6, color='g')
575
+
576
+ plt.show()
577
+
578
+ return fig, ax
579
+
580
+
581
+ if __name__ == '__main__':
582
+ lognorm_dist()
@@ -0,0 +1 @@
1
+ from .improve import *