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