AeroViz 0.1.3__py3-none-any.whl → 0.1.4__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 (121) hide show
  1. AeroViz/__init__.py +7 -5
  2. AeroViz/{config → data}/DEFAULT_DATA.csv +1 -1
  3. AeroViz/dataProcess/Chemistry/__init__.py +40 -40
  4. AeroViz/dataProcess/Chemistry/_calculate.py +15 -15
  5. AeroViz/dataProcess/Chemistry/_isoropia.py +72 -68
  6. AeroViz/dataProcess/Chemistry/_mass_volume.py +158 -161
  7. AeroViz/dataProcess/Chemistry/_ocec.py +109 -109
  8. AeroViz/dataProcess/Chemistry/_partition.py +19 -18
  9. AeroViz/dataProcess/Chemistry/_teom.py +9 -11
  10. AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
  11. AeroViz/dataProcess/Optical/Angstrom_exponent.py +20 -0
  12. AeroViz/dataProcess/Optical/_IMPROVE.py +40 -41
  13. AeroViz/dataProcess/Optical/__init__.py +29 -44
  14. AeroViz/dataProcess/Optical/_absorption.py +21 -47
  15. AeroViz/dataProcess/Optical/_extinction.py +31 -25
  16. AeroViz/dataProcess/Optical/_mie.py +5 -7
  17. AeroViz/dataProcess/Optical/_mie_sd.py +89 -90
  18. AeroViz/dataProcess/Optical/_scattering.py +19 -20
  19. AeroViz/dataProcess/SizeDistr/__init__.py +39 -39
  20. AeroViz/dataProcess/SizeDistr/__merge.py +159 -158
  21. AeroViz/dataProcess/SizeDistr/_merge.py +155 -154
  22. AeroViz/dataProcess/SizeDistr/_merge_v1.py +162 -161
  23. AeroViz/dataProcess/SizeDistr/_merge_v2.py +153 -152
  24. AeroViz/dataProcess/SizeDistr/_merge_v3.py +327 -327
  25. AeroViz/dataProcess/SizeDistr/_merge_v4.py +273 -275
  26. AeroViz/dataProcess/SizeDistr/_size_distr.py +51 -51
  27. AeroViz/dataProcess/VOC/__init__.py +9 -9
  28. AeroViz/dataProcess/VOC/_potential_par.py +53 -55
  29. AeroViz/dataProcess/__init__.py +28 -6
  30. AeroViz/dataProcess/core/__init__.py +59 -65
  31. AeroViz/plot/__init__.py +7 -2
  32. AeroViz/plot/bar.py +126 -0
  33. AeroViz/plot/box.py +69 -0
  34. AeroViz/plot/distribution/distribution.py +421 -427
  35. AeroViz/plot/meteorology/meteorology.py +240 -292
  36. AeroViz/plot/optical/__init__.py +0 -1
  37. AeroViz/plot/optical/optical.py +230 -230
  38. AeroViz/plot/pie.py +198 -0
  39. AeroViz/plot/regression.py +196 -0
  40. AeroViz/plot/scatter.py +165 -0
  41. AeroViz/plot/templates/__init__.py +2 -4
  42. AeroViz/plot/templates/ammonium_rich.py +34 -0
  43. AeroViz/plot/templates/contour.py +25 -25
  44. AeroViz/plot/templates/corr_matrix.py +86 -93
  45. AeroViz/plot/templates/diurnal_pattern.py +28 -26
  46. AeroViz/plot/templates/koschmieder.py +59 -123
  47. AeroViz/plot/templates/metal_heatmap.py +135 -37
  48. AeroViz/plot/timeseries/__init__.py +1 -0
  49. AeroViz/plot/timeseries/template.py +47 -0
  50. AeroViz/plot/timeseries/timeseries.py +324 -264
  51. AeroViz/plot/utils/__init__.py +2 -1
  52. AeroViz/plot/utils/_color.py +57 -57
  53. AeroViz/plot/utils/_unit.py +48 -48
  54. AeroViz/plot/utils/plt_utils.py +92 -0
  55. AeroViz/plot/utils/sklearn_utils.py +49 -0
  56. AeroViz/plot/utils/units.json +5 -0
  57. AeroViz/plot/violin.py +80 -0
  58. AeroViz/process/__init__.py +17 -17
  59. AeroViz/process/core/DataProc.py +9 -9
  60. AeroViz/process/core/SizeDist.py +81 -81
  61. AeroViz/process/method/PyMieScatt_update.py +488 -488
  62. AeroViz/process/method/mie_theory.py +231 -229
  63. AeroViz/process/method/prop.py +40 -40
  64. AeroViz/process/script/AbstractDistCalc.py +103 -103
  65. AeroViz/process/script/Chemical.py +168 -167
  66. AeroViz/process/script/IMPACT.py +40 -40
  67. AeroViz/process/script/IMPROVE.py +152 -152
  68. AeroViz/process/script/Others.py +45 -45
  69. AeroViz/process/script/PSD.py +26 -26
  70. AeroViz/process/script/PSD_dry.py +69 -70
  71. AeroViz/process/script/retrieve_RI.py +50 -51
  72. AeroViz/rawDataReader/__init__.py +53 -58
  73. AeroViz/rawDataReader/config/supported_instruments.py +155 -0
  74. AeroViz/rawDataReader/core/__init__.py +233 -356
  75. AeroViz/rawDataReader/script/AE33.py +17 -18
  76. AeroViz/rawDataReader/script/AE43.py +18 -21
  77. AeroViz/rawDataReader/script/APS_3321.py +30 -30
  78. AeroViz/rawDataReader/script/Aurora.py +23 -24
  79. AeroViz/rawDataReader/script/BC1054.py +36 -40
  80. AeroViz/rawDataReader/script/EPA_vertical.py +37 -9
  81. AeroViz/rawDataReader/script/GRIMM.py +16 -23
  82. AeroViz/rawDataReader/script/IGAC.py +90 -0
  83. AeroViz/rawDataReader/script/MA350.py +32 -39
  84. AeroViz/rawDataReader/script/Minion.py +103 -0
  85. AeroViz/rawDataReader/script/NEPH.py +69 -74
  86. AeroViz/rawDataReader/script/SMPS_TH.py +25 -25
  87. AeroViz/rawDataReader/script/SMPS_aim11.py +32 -32
  88. AeroViz/rawDataReader/script/SMPS_genr.py +31 -31
  89. AeroViz/rawDataReader/script/Sunset_OCEC.py +60 -0
  90. AeroViz/rawDataReader/script/TEOM.py +30 -28
  91. AeroViz/rawDataReader/script/Table.py +13 -14
  92. AeroViz/rawDataReader/script/VOC.py +26 -0
  93. AeroViz/rawDataReader/script/__init__.py +18 -20
  94. AeroViz/tools/database.py +64 -66
  95. AeroViz/tools/dataclassifier.py +106 -106
  96. AeroViz/tools/dataprinter.py +51 -51
  97. AeroViz/tools/datareader.py +38 -38
  98. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/METADATA +5 -4
  99. AeroViz-0.1.4.dist-info/RECORD +112 -0
  100. AeroViz/plot/improve/__init__.py +0 -1
  101. AeroViz/plot/improve/improve.py +0 -240
  102. AeroViz/plot/optical/aethalometer.py +0 -77
  103. AeroViz/plot/templates/event_evolution.py +0 -65
  104. AeroViz/plot/templates/regression.py +0 -256
  105. AeroViz/plot/templates/scatter.py +0 -130
  106. AeroViz/plot/templates/templates.py +0 -398
  107. AeroViz/plot/utils/_decorator.py +0 -74
  108. AeroViz/rawDataReader/script/IGAC_TH.py +0 -104
  109. AeroViz/rawDataReader/script/IGAC_ZM.py +0 -90
  110. AeroViz/rawDataReader/script/OCEC_LCRES.py +0 -34
  111. AeroViz/rawDataReader/script/OCEC_RES.py +0 -28
  112. AeroViz/rawDataReader/script/VOC_TH.py +0 -30
  113. AeroViz/rawDataReader/script/VOC_ZM.py +0 -37
  114. AeroViz/rawDataReader/utils/__init__.py +0 -0
  115. AeroViz/rawDataReader/utils/config.py +0 -169
  116. AeroViz-0.1.3.dist-info/RECORD +0 -111
  117. /AeroViz/{config → data}/DEFAULT_PNSD_DATA.csv +0 -0
  118. /AeroViz/{config → rawDataReader/config}/__init__.py +0 -0
  119. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/LICENSE +0 -0
  120. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/WHEEL +0 -0
  121. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,9 @@
1
1
  import math
2
2
  from typing import Literal
3
3
 
4
- import matplotlib.colors as plc
5
4
  import matplotlib.pyplot as plt
6
5
  import numpy as np
7
6
  import pandas as pd
8
- import seaborn as sns
9
7
  import windrose
10
8
  from matplotlib.pyplot import Figure, Axes
11
9
  from pandas import DataFrame, Series
@@ -13,305 +11,255 @@ from scipy.ndimage import gaussian_filter
13
11
 
14
12
  from AeroViz.plot.utils import *
15
13
 
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
14
+ __all__ = ['wind_rose',
15
+ 'CBPF'
16
+ ]
71
17
 
72
18
 
73
19
  @set_figure(figsize=(4.3, 4))
74
20
  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
21
+ WS: Series | str,
22
+ WD: Series | str,
23
+ val: Series | str | None = None,
24
+ typ: Literal['bar', 'scatter'] = 'scatter',
25
+ rlabel_pos: float = 30,
26
+ **kwargs
27
+ ) -> tuple[Figure, Axes]:
28
+ # conditional bivariate probability function (cbpf) python
29
+ # https://davidcarslaw.github.io/openair/reference/polarPlot.html
30
+ # https://github.com/davidcarslaw/openair/blob/master/R/polarPlot.R
31
+ windrose.WindroseAxes._info = 'WindroseAxes'
32
+
33
+ df = df.dropna(subset=[WS, WD] + ([val] if val is not None else []))
34
+
35
+ radius = df[WS].to_numpy()
36
+ theta = df[WD].to_numpy()
37
+ radian = np.radians(theta)
38
+ values = df[val].to_numpy() if val is not None else None
39
+
40
+ # In this case, the windrose is a simple frequency diagram,
41
+ # the function automatically calculates the radians of the given wind direction.
42
+ if typ == 'bar':
43
+ fig, ax = plt.subplots(figsize=(5.5, 4), subplot_kw={'projection': 'windrose'})
44
+ fig.subplots_adjust(left=0)
45
+
46
+ ax.bar(theta, radius, bins=[0, 1, 2, 3], normed=True, colors=['#0F1035', '#365486', '#7FC7D9', '#DCF2F1'])
47
+ ax.set(
48
+ ylim=(0, 30),
49
+ yticks=[0, 15, 30],
50
+ yticklabels=['', '15 %', '30 %'],
51
+ rlabel_position=rlabel_pos
52
+ )
53
+ ax.set_thetagrids(angles=[0, 45, 90, 135, 180, 225, 270, 315],
54
+ labels=["E", "NE", "N", "NW", "W", "SW", "S", "SE"])
55
+
56
+ ax.legend(units='m/s', bbox_to_anchor=[1.1, 0.5], loc='center left', ncol=1)
57
+
58
+ # In this case, the windrose is a scatter plot,
59
+ # in contrary, this function does not calculate the radians, so user have to input the radian.
60
+ else:
61
+ fig, ax = plt.subplots(figsize=(5, 4), subplot_kw={'projection': 'windrose'})
62
+ fig.subplots_adjust(left=0)
63
+
64
+ scatter = ax.scatter(radian, radius, s=15, c=values, vmax=np.quantile(values, 0.90), edgecolors='none',
65
+ cmap='jet', alpha=0.8)
66
+ ax.set(
67
+ ylim=(0, 7),
68
+ yticks=[1, 3, 5, 7],
69
+ yticklabels=['1 m/s', '3 m/s', '5 m/s', '7 m/s'],
70
+ rlabel_position=rlabel_pos,
71
+ theta_direction=-1,
72
+ theta_zero_location='N',
73
+ )
74
+ ax.set_thetagrids(angles=[0, 45, 90, 135, 180, 225, 270, 315],
75
+ labels=["N", "NE", "E", "SE", "S", "SW", "W", "NW"])
76
+
77
+ plt.colorbar(scatter, ax=ax, label=Unit(val), pad=0.1, fraction=0.04)
78
+
79
+ plt.show()
80
+
81
+ return fig, ax
136
82
 
137
83
 
138
84
  @set_figure(figsize=(4.3, 4))
139
85
  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)
86
+ WS: Series | str,
87
+ WD: Series | str,
88
+ val: Series | str | None = None,
89
+ percentile: list | float | int | None = None,
90
+ max_ws: float | None = 5,
91
+ resolution: int = 100,
92
+ sigma: float | tuple = 2,
93
+ rlabel_pos: float = 30,
94
+ bottom_text: str | bool | None = None,
95
+ **kwargs
96
+ ) -> tuple[Figure, Axes]:
97
+ # conditional bivariate probability function (cbpf) python
98
+ # https://davidcarslaw.github.io/openair/reference/polarPlot.html
99
+ # https://github.com/davidcarslaw/openair/blob/master/R/polarPlot.R
100
+
101
+ df = df.dropna(subset=[WS, WD] + ([val] if val is not None else [])).copy()
102
+
103
+ df['u'] = df[WS].to_numpy() * np.sin(np.radians(df[WD].to_numpy()))
104
+ df['v'] = df[WS].to_numpy() * np.cos(np.radians(df[WD].to_numpy()))
105
+
106
+ u_bins = np.linspace(df.u.min(), df.u.max(), resolution)
107
+ v_bins = np.linspace(df.v.min(), df.v.max(), resolution)
108
+
109
+ # 使用 u_group 和 v_group 進行分組
110
+ df['u_group'] = pd.cut(df['u'], u_bins)
111
+ df['v_group'] = pd.cut(df['v'], v_bins)
112
+ grouped = df.groupby(['u_group', 'v_group'], observed=False)
113
+
114
+ X, Y = np.meshgrid(u_bins, v_bins)
115
+
116
+ # Note:
117
+ # The CBPF is the ratio between the number of points in each cell and the total number of points.
118
+ # So, it is not equal to the probability density function (PDF) of the wind speed and wind direction.
119
+
120
+ if percentile is None:
121
+ histogram = (grouped[val].count() / grouped[val].count().sum()).unstack().values.T
122
+ # histogram, v_edges, u_edges = np.histogram2d(df.v, df.u, bins=(v_bins, u_bins))
123
+ # histogram = histogram / histogram.sum()
124
+ histogram = np.where(histogram == 0, np.nan, histogram)
125
+ bottom_text = rf'$PDF\ plot$'
126
+
127
+ else:
128
+ if not all(0 <= p <= 100 for p in (percentile if isinstance(percentile, list) else [percentile])):
129
+ raise ValueError("Percentile must be between 0 and 100")
130
+
131
+ if isinstance(percentile, (float, int)):
132
+ bottom_text = rf'$CPF:\ >{int(percentile)}^{{th}}$'
133
+ thershold = df[val].quantile(percentile / 100)
134
+ cond = lambda x: (x >= thershold).sum()
135
+
136
+ elif isinstance(percentile, list) and len(percentile) == 1:
137
+ # Extract the single element from the list
138
+ single_percentile = percentile[0]
139
+ bottom_text = rf'$CPF:\ >{int(single_percentile)}^{{th}}$'
140
+ threshold = df[val].quantile(single_percentile / 100)
141
+ cond = lambda x: (x >= threshold).sum()
142
+
143
+ else:
144
+ bottom_text = rf'$CPF:\ {int(percentile[0])}^{{th}}\ to\ {int(percentile[1])}^{{th}}$'
145
+ thershold_small, thershold_large = df[val].quantile([percentile[0] / 100, percentile[1] / 100])
146
+ cond = lambda x: ((x >= thershold_small) & (x < thershold_large)).sum()
147
+
148
+ histogram = (grouped[val].apply(cond) / grouped[val].count()).unstack().values.T
149
+
150
+ # if np.isnan(histogram).all():
151
+ # raise "CBPF_array contains only NaN values."
152
+ # else:
153
+ # print(f"\nHistogram contains NaN before masking: {np.isnan(histogram).sum()}")
154
+
155
+ histogram_filled = np.nan_to_num(histogram, nan=0) # 將 NaN 替換為 0
156
+
157
+ filtered_histogram = gaussian_filter(histogram_filled, sigma=sigma)
158
+ filtered_histogram[np.isnan(histogram)] = np.nan
159
+
160
+ def is_within_circle(center_row, center_col, row, col, radius):
161
+ return np.sqrt((center_row - row) ** 2 + (center_col - col) ** 2) <= radius
162
+
163
+ def remove_lonely_point(filtered_histogram, radius=4, magic_num=13):
164
+ rows, cols = filtered_histogram.shape
165
+ data_positions = np.where(~np.isnan(filtered_histogram))
166
+
167
+ for row, col in zip(*data_positions):
168
+ valid_data_count = 0
169
+ for i in range(max(0, row - radius), min(rows, row + radius + 1)):
170
+ for j in range(max(0, col - radius), min(cols, col + radius + 1)):
171
+ if (i, j) != (row, col) and is_within_circle(row, col, i, j, radius):
172
+ if not np.isnan(filtered_histogram[i, j]):
173
+ valid_data_count += 1
174
+
175
+ if valid_data_count <= magic_num:
176
+ filtered_histogram[row, col] = np.nan
177
+
178
+ return filtered_histogram
179
+
180
+ def fill_nan_with_mean(filtered_histogram, radius=4, magic_num=13):
181
+ rows, cols = filtered_histogram.shape
182
+ nan_positions = np.where(np.isnan(filtered_histogram))
183
+
184
+ for row, col in zip(*nan_positions):
185
+ surrounding_values = []
186
+ surrounding_values_within_one = []
187
+ nan_count = 0
188
+
189
+ for i in range(max(0, row - radius), min(rows, row + radius + 1)):
190
+ for j in range(max(0, col - radius), min(cols, col + radius + 1)):
191
+ if (i, j) != (row, col) and is_within_circle(row, col, i, j, radius):
192
+ if np.isnan(filtered_histogram[i, j]):
193
+ nan_count += 1
194
+ else:
195
+ surrounding_values.append(filtered_histogram[i, j])
196
+
197
+ for i in range(max(0, row - 2), min(rows, row + 2 + 1)):
198
+ for j in range(max(0, col - 2), min(cols, col + 2 + 1)):
199
+ if (i, j) != (row, col) and is_within_circle(row, col, i, j, 2):
200
+ if np.isnan(filtered_histogram[i, j]):
201
+ pass
202
+ else:
203
+ surrounding_values_within_one.append(filtered_histogram[i, j])
204
+
205
+ if nan_count < magic_num and surrounding_values_within_one:
206
+ filtered_histogram[row, col] = np.mean(surrounding_values)
207
+
208
+ return filtered_histogram
209
+
210
+ # Apply the function to your data
211
+ fil_radius, magic_num = 3, 13
212
+ filtered_histogram = remove_lonely_point(filtered_histogram, fil_radius, magic_num)
213
+ filtered_histogram = fill_nan_with_mean(filtered_histogram, fil_radius, magic_num)
214
+ if np.all(np.isnan(filtered_histogram)):
215
+ raise ValueError("All values in the filtered histogram are NaN. Please decrease the resolution.")
216
+ # plot
217
+ fig, ax = plt.subplots()
218
+ fig.subplots_adjust(left=0)
219
+
220
+ surf = ax.pcolormesh(X, Y, filtered_histogram, shading='auto', cmap='jet', antialiased=True)
221
+
222
+ max_ws = max_ws or np.concatenate((abs(df.u), abs(df.v))).max() # Get the maximum value of the wind speed
223
+
224
+ radius_lst = np.arange(1, math.ceil(max_ws) + 1) # Create a list of radius
225
+
226
+ for i, radius in enumerate(radius_lst):
227
+ circle = plt.Circle((0, 0), radius, fill=False, color='gray', linewidth=1, linestyle='--', alpha=0.5)
228
+ ax.add_artist(circle)
229
+
230
+ for angle, label in zip(range(0, 360, 90), ["E", "N", "W", "S"]):
231
+ radian = np.radians(angle)
232
+ line_x, line_y = radius * np.cos(radian), radius * np.sin(radian)
233
+
234
+ if i + 2 == len(radius_lst): # Add wind direction line and direction label at the edge of the circle
235
+ ax.plot([0, line_x * 1.05], [0, line_y * 1.05], color='k', linestyle='-', linewidth=1, alpha=0.5)
236
+ ax.text(line_x * 1.15, line_y * 1.15, label, ha='center', va='center')
237
+
238
+ ax.text(radius * np.cos(np.radians(rlabel_pos)), radius * np.sin(np.radians(rlabel_pos)),
239
+ str(radius) + ' m/s', ha='center', va='center', fontsize=8)
267
240
 
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')
241
+ for radius in range(math.ceil(max_ws) + 1, 10):
242
+ circle = plt.Circle((0, 0), radius, fill=False, color='gray', linewidth=1, linestyle='--', alpha=0.5)
243
+ ax.add_artist(circle)
289
244
 
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)
245
+ ax.set(xlim=(-max_ws * 1.02, max_ws * 1.02),
246
+ ylim=(-max_ws * 1.02, max_ws * 1.02),
247
+ xticks=[],
248
+ yticks=[],
249
+ xticklabels=[],
250
+ yticklabels=[],
251
+ aspect='equal')
292
252
 
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)
253
+ if bottom_text:
254
+ ax.text(0.50, -0.05, bottom_text, fontweight='bold', fontsize=8, va='center', ha='center',
255
+ transform=ax.transAxes)
296
256
 
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
257
+ ax.text(0.5, 1.05, Unit(val), fontweight='bold', fontsize=12, va='center', ha='center', transform=ax.transAxes)
258
+
259
+ cbar = plt.colorbar(surf, ax=ax, label='Frequency', pad=0.01, fraction=0.04)
260
+ cbar.ax.yaxis.label.set_fontsize(8)
261
+ cbar.ax.tick_params(labelsize=8)
262
+
263
+ plt.show()
264
+
265
+ return fig, ax
@@ -1,2 +1 @@
1
- from .aethalometer import *
2
1
  from .optical import *