AeroViz 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of AeroViz might be problematic. Click here for more details.
- AeroViz/dataProcess/Chemistry/_ocec.py +20 -7
- AeroViz/plot/__init__.py +1 -0
- AeroViz/plot/meteorology/meteorology.py +2 -0
- AeroViz/plot/optical/optical.py +1 -1
- AeroViz/plot/pie.py +14 -2
- AeroViz/plot/radar.py +184 -0
- AeroViz/plot/scatter.py +16 -7
- AeroViz/plot/templates/koschmieder.py +11 -8
- AeroViz/plot/timeseries/timeseries.py +0 -1
- AeroViz/rawDataReader/__init__.py +74 -67
- AeroViz/rawDataReader/config/supported_instruments.py +52 -19
- AeroViz/rawDataReader/core/__init__.py +129 -104
- AeroViz/rawDataReader/script/AE33.py +1 -1
- AeroViz/rawDataReader/script/AE43.py +1 -1
- AeroViz/rawDataReader/script/Aurora.py +1 -1
- AeroViz/rawDataReader/script/BC1054.py +1 -1
- AeroViz/rawDataReader/script/EPA.py +39 -0
- AeroViz/rawDataReader/script/GRIMM.py +1 -1
- AeroViz/rawDataReader/script/IGAC.py +6 -23
- AeroViz/rawDataReader/script/MA350.py +1 -1
- AeroViz/rawDataReader/script/Minion.py +102 -30
- AeroViz/rawDataReader/script/NEPH.py +1 -1
- AeroViz/rawDataReader/script/OCEC.py +1 -1
- AeroViz/rawDataReader/script/SMPS.py +1 -0
- AeroViz/rawDataReader/script/TEOM.py +2 -2
- AeroViz/rawDataReader/script/XRF.py +11 -0
- AeroViz/rawDataReader/script/__init__.py +2 -2
- {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/METADATA +46 -24
- {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/RECORD +32 -48
- AeroViz/process/__init__.py +0 -31
- AeroViz/process/core/DataProc.py +0 -19
- AeroViz/process/core/SizeDist.py +0 -90
- AeroViz/process/core/__init__.py +0 -4
- AeroViz/process/method/PyMieScatt_update.py +0 -567
- AeroViz/process/method/__init__.py +0 -2
- AeroViz/process/method/mie_theory.py +0 -260
- AeroViz/process/method/prop.py +0 -62
- AeroViz/process/script/AbstractDistCalc.py +0 -143
- AeroViz/process/script/Chemical.py +0 -177
- AeroViz/process/script/IMPACT.py +0 -49
- AeroViz/process/script/IMPROVE.py +0 -161
- AeroViz/process/script/Others.py +0 -65
- AeroViz/process/script/PSD.py +0 -103
- AeroViz/process/script/PSD_dry.py +0 -93
- AeroViz/process/script/__init__.py +0 -5
- AeroViz/process/script/retrieve_RI.py +0 -69
- AeroViz/rawDataReader/script/EPA_vertical.py +0 -46
- AeroViz/rawDataReader/script/Table.py +0 -27
- {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/LICENSE +0 -0
- {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/WHEEL +0 -0
- {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
from pandas import concat, DataFrame
|
|
3
|
-
from scipy.optimize import curve_fit
|
|
5
|
+
from scipy.optimize import curve_fit, least_squares, OptimizeWarning
|
|
4
6
|
|
|
5
7
|
from AeroViz.dataProcess.core import union_index
|
|
6
8
|
|
|
@@ -22,15 +24,26 @@ def _min_Rsq(_oc, _ec, _rng):
|
|
|
22
24
|
for _ocec, _out in _out_table.items():
|
|
23
25
|
_df = DataFrame([_out.values, _ec.values]).T.dropna()
|
|
24
26
|
|
|
25
|
-
_x, _y = _df[0], _df[1]
|
|
26
|
-
|
|
27
|
+
_x, _y = _df[0].values, _df[1].values
|
|
28
|
+
|
|
29
|
+
# 初始參數估計
|
|
30
|
+
slope_guess = (_y[-1] - _y[0]) / (_x[-1] - _x[0])
|
|
31
|
+
intercept_guess = _y[0] - slope_guess * _x[0]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with warnings.catch_warnings():
|
|
35
|
+
warnings.filterwarnings('error')
|
|
36
|
+
_opt, _ = curve_fit(_func, _x, _y, p0=[slope_guess, intercept_guess], maxfev=5000)
|
|
37
|
+
except (RuntimeWarning, OptimizeWarning):
|
|
38
|
+
# 如果 curve_fit 失敗,嘗試使用 least_squares
|
|
39
|
+
residuals = lambda p: _func(_x, *p) - _y
|
|
40
|
+
_opt = least_squares(residuals, [slope_guess, intercept_guess]).x
|
|
27
41
|
|
|
28
|
-
_tss = np.sum((_y -
|
|
29
|
-
_rss = np.sum((_y - _func(_x, *_opt)) ** 2
|
|
42
|
+
_tss = np.sum((_y - np.mean(_y)) ** 2)
|
|
43
|
+
_rss = np.sum((_y - _func(_x, *_opt)) ** 2)
|
|
30
44
|
|
|
31
|
-
_r2_dic[round(_ocec, 3)] = 1
|
|
45
|
+
_r2_dic[round(_ocec, 3)] = 1 - _rss / _tss
|
|
32
46
|
|
|
33
|
-
# get the min R2
|
|
34
47
|
_ratio = DataFrame(_r2_dic, index=[0]).idxmin(axis=1).values[0]
|
|
35
48
|
|
|
36
49
|
return _ratio, _out_table[_ratio]
|
AeroViz/plot/__init__.py
CHANGED
|
@@ -70,6 +70,7 @@ def wind_rose(df: DataFrame,
|
|
|
70
70
|
rlabel_position=rlabel_pos,
|
|
71
71
|
theta_direction=-1,
|
|
72
72
|
theta_zero_location='N',
|
|
73
|
+
title=kwargs.get('title', None)
|
|
73
74
|
)
|
|
74
75
|
ax.set_thetagrids(angles=[0, 45, 90, 135, 180, 225, 270, 315],
|
|
75
76
|
labels=["N", "NE", "E", "SE", "S", "SW", "W", "NW"])
|
|
@@ -81,6 +82,7 @@ def wind_rose(df: DataFrame,
|
|
|
81
82
|
return fig, ax
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
# TODO: fix the bug of the CBPF function
|
|
84
86
|
@set_figure(figsize=(4.3, 4))
|
|
85
87
|
def CBPF(df: DataFrame,
|
|
86
88
|
WS: Series | str,
|
AeroViz/plot/optical/optical.py
CHANGED
|
@@ -7,7 +7,7 @@ import numpy as np
|
|
|
7
7
|
from matplotlib.pyplot import Figure, Axes
|
|
8
8
|
|
|
9
9
|
from AeroViz.plot.utils import *
|
|
10
|
-
from
|
|
10
|
+
from temp.process.method.mie_theory import Mie_Q, Mie_MEE, Mie_PESD
|
|
11
11
|
|
|
12
12
|
__all__ = ['Q_plot',
|
|
13
13
|
'RI_couple',
|
AeroViz/plot/pie.py
CHANGED
|
@@ -81,7 +81,9 @@ def pie(data_set: DataFrame | dict,
|
|
|
81
81
|
radius = 4
|
|
82
82
|
width = 4 if style == 'pie' else 1
|
|
83
83
|
|
|
84
|
-
text = [''] * pies if style == 'pie' else [Unit(unit) + '\n\n' +
|
|
84
|
+
text = [''] * pies if style == 'pie' else [Unit(unit) + '\n\n' +
|
|
85
|
+
'{:.2f} ± {:.2f}'.format(x, s)
|
|
86
|
+
for x, s in zip(data.sum(axis=1), data.std(axis=1))]
|
|
85
87
|
pct_distance = 0.6 if style == 'pie' else 0.88
|
|
86
88
|
|
|
87
89
|
fig, ax = plt.subplots(1, pies, figsize=((pies * 2) + 1, 2)) if ax is None else (ax.get_figure(), ax)
|
|
@@ -99,7 +101,17 @@ def pie(data_set: DataFrame | dict,
|
|
|
99
101
|
pctdistance=1.3, radius=radius, wedgeprops=dict(width=width, edgecolor='w'))
|
|
100
102
|
ax[i].axis('equal')
|
|
101
103
|
ax[i].text(0, 0, text[i], ha='center', va='center')
|
|
102
|
-
|
|
104
|
+
|
|
105
|
+
if kwargs.get('title') is None:
|
|
106
|
+
ax[i].set_title(category_names[i])
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
if len(kwargs.get('title')) == pies:
|
|
110
|
+
title = kwargs.get('title')
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError('The length of the title list must match the number of pies.')
|
|
113
|
+
|
|
114
|
+
ax[i].set_title(title[i])
|
|
103
115
|
|
|
104
116
|
ax[-1].legend(labels, loc='center left', prop={'size': 8, 'weight': 'normal'}, bbox_to_anchor=(1, 0, 1.15, 1))
|
|
105
117
|
|
AeroViz/plot/radar.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
from matplotlib.patches import Circle, RegularPolygon
|
|
4
|
+
from matplotlib.path import Path
|
|
5
|
+
from matplotlib.projections import register_projection
|
|
6
|
+
from matplotlib.projections.polar import PolarAxes
|
|
7
|
+
from matplotlib.spines import Spine
|
|
8
|
+
from matplotlib.transforms import Affine2D
|
|
9
|
+
|
|
10
|
+
from AeroViz.plot.utils import *
|
|
11
|
+
|
|
12
|
+
__all__ = ['radar']
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def radar_factory(num_vars, frame='circle'):
|
|
16
|
+
"""
|
|
17
|
+
Create a radar chart with `num_vars` axes.
|
|
18
|
+
|
|
19
|
+
This function creates a RadarAxes projection and registers it.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
num_vars : int
|
|
24
|
+
Number of variables for radar chart.
|
|
25
|
+
frame : {'circle', 'polygon'}
|
|
26
|
+
Shape of frame surrounding axes.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
# calculate evenly-spaced axis angles
|
|
30
|
+
theta = np.linspace(0, 2 * np.pi, num_vars, endpoint=False)
|
|
31
|
+
|
|
32
|
+
class RadarTransform(PolarAxes.PolarTransform):
|
|
33
|
+
|
|
34
|
+
def transform_path_non_affine(self, path):
|
|
35
|
+
# Paths with non-unit interpolation steps correspond to gridlines,
|
|
36
|
+
# in which case we force interpolation (to defeat PolarTransform's
|
|
37
|
+
# autoconversion to circular arcs).
|
|
38
|
+
if path._interpolation_steps > 1:
|
|
39
|
+
path = path.interpolated(num_vars)
|
|
40
|
+
return Path(self.transform(path.vertices), path.codes)
|
|
41
|
+
|
|
42
|
+
class RadarAxes(PolarAxes):
|
|
43
|
+
|
|
44
|
+
name = 'radar'
|
|
45
|
+
PolarTransform = RadarTransform
|
|
46
|
+
|
|
47
|
+
def __init__(self, *args, **kwargs):
|
|
48
|
+
super().__init__(*args, **kwargs)
|
|
49
|
+
# rotate plot such that the first axis is at the top
|
|
50
|
+
self.set_theta_zero_location('N')
|
|
51
|
+
|
|
52
|
+
def fill(self, *args, closed=True, **kwargs):
|
|
53
|
+
"""Override fill so that line is closed by default"""
|
|
54
|
+
return super().fill(closed=closed, *args, **kwargs)
|
|
55
|
+
|
|
56
|
+
def plot(self, *args, **kwargs):
|
|
57
|
+
"""Override plot so that line is closed by default"""
|
|
58
|
+
lines = super().plot(*args, **kwargs)
|
|
59
|
+
for line in lines:
|
|
60
|
+
self._close_line(line)
|
|
61
|
+
|
|
62
|
+
def _close_line(self, line):
|
|
63
|
+
x, y = line.get_data()
|
|
64
|
+
if x[0] != x[-1]:
|
|
65
|
+
x = np.append(x, x[0])
|
|
66
|
+
y = np.append(y, y[0])
|
|
67
|
+
line.set_data(x, y)
|
|
68
|
+
|
|
69
|
+
def set_varlabels(self, labels):
|
|
70
|
+
self.set_thetagrids(np.degrees(theta), labels)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _gen_axes_patch():
|
|
74
|
+
# The Axes patch must be centered at (0.5, 0.5) and of radius 0.5
|
|
75
|
+
# in axes coordinates.
|
|
76
|
+
if frame == 'circle':
|
|
77
|
+
return Circle((0.5, 0.5), 0.5)
|
|
78
|
+
elif frame == 'polygon':
|
|
79
|
+
return RegularPolygon((0.5, 0.5), num_vars,
|
|
80
|
+
radius=.5, edgecolor="k")
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError("Unknown value for 'frame': %s" % frame)
|
|
83
|
+
|
|
84
|
+
def _gen_axes_spines(self):
|
|
85
|
+
if frame == 'circle':
|
|
86
|
+
return super()._gen_axes_spines()
|
|
87
|
+
elif frame == 'polygon':
|
|
88
|
+
# spine_type must be 'left'/'right'/'top'/'bottom'/'circle'.
|
|
89
|
+
spine = Spine(axes=self,
|
|
90
|
+
spine_type='circle',
|
|
91
|
+
path=Path.unit_regular_polygon(num_vars))
|
|
92
|
+
# unit_regular_polygon gives a polygon of radius 1 centered at
|
|
93
|
+
# (0, 0) but we want a polygon of radius 0.5 centered at (0.5,
|
|
94
|
+
# 0.5) in axes coordinates.
|
|
95
|
+
spine.set_transform(Affine2D().scale(.5).translate(.5, .5)
|
|
96
|
+
+ self.transAxes)
|
|
97
|
+
return {'polar': spine}
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError("Unknown value for 'frame': %s" % frame)
|
|
100
|
+
|
|
101
|
+
register_projection(RadarAxes)
|
|
102
|
+
return theta
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@set_figure(figsize=(3, 3))
|
|
106
|
+
def radar(data, labels=None, legend_labels=None, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
|
107
|
+
"""
|
|
108
|
+
Creates a radar chart based on the provided data.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
data : list of list
|
|
113
|
+
A 2D list where each inner list represents a factor, and each element
|
|
114
|
+
within the inner lists represents a value for a species.
|
|
115
|
+
Shape: (n_factors, n_species)
|
|
116
|
+
Example: [[0.88, 0.01, 0.03, ...], [0.07, 0.95, 0.04, ...], ...]
|
|
117
|
+
|
|
118
|
+
labels : list, optional
|
|
119
|
+
A list of strings representing the names of species (variables).
|
|
120
|
+
If provided, it should have the same length as the number of elements
|
|
121
|
+
in each inner list of `data`.
|
|
122
|
+
Example: ['Sulfate', 'Nitrate', 'EC', 'OC1', 'OC2', 'OP', 'CO', 'O3']
|
|
123
|
+
|
|
124
|
+
legend_labels : list, optional
|
|
125
|
+
A list of strings for labeling each factor in the legend.
|
|
126
|
+
If provided, it should have the same length as the number of inner lists in `data`.
|
|
127
|
+
|
|
128
|
+
**kwargs : dict
|
|
129
|
+
Additional keyword arguments to be passed to the plotting function.
|
|
130
|
+
This may include 'title' for setting the chart title.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
tuple[plt.Figure, plt.Axes]
|
|
135
|
+
A tuple containing the Figure and Axes objects of the created plot.
|
|
136
|
+
|
|
137
|
+
Example
|
|
138
|
+
-------
|
|
139
|
+
>>> data = [[0.88, 0.01, 0.03, 0.03, 0.00, 0.06, 0.01, 0.00],
|
|
140
|
+
>>> [0.07, 0.95, 0.04, 0.05, 0.00, 0.02, 0.01, 0.00],
|
|
141
|
+
>>> [0.01, 0.02, 0.85, 0.19, 0.05, 0.10, 0.00, 0.00],
|
|
142
|
+
>>> [0.02, 0.01, 0.07, 0.01, 0.21, 0.12, 0.98, 0.00],
|
|
143
|
+
>>> [0.01, 0.01, 0.02, 0.71, 0.74, 0.70, 0.30, 0.20]]
|
|
144
|
+
>>> labels = ['Sulfate', 'Nitrate', 'EC', 'OC1', 'OC2', 'OP', 'CO', 'O3']
|
|
145
|
+
>>> fig, ax = radar(data, labels=labels, title='Basecase')
|
|
146
|
+
|
|
147
|
+
Note
|
|
148
|
+
----
|
|
149
|
+
The first dimension of `data` represents each factor, while the second
|
|
150
|
+
dimension represents each species.
|
|
151
|
+
"""
|
|
152
|
+
theta = radar_factory(np.array(data).shape[1], frame='polygon')
|
|
153
|
+
|
|
154
|
+
fig, ax = plt.subplots(subplot_kw=dict(projection='radar'))
|
|
155
|
+
fig.subplots_adjust(wspace=0.25, hspace=0.20, top=0.80, bottom=0.05, right=0.80)
|
|
156
|
+
|
|
157
|
+
colors = ['b', 'r', 'g', 'm', 'y']
|
|
158
|
+
|
|
159
|
+
# Plot the four cases from the example data on separate axes
|
|
160
|
+
for d, color in zip(data, colors):
|
|
161
|
+
ax.plot(theta, d, color=color)
|
|
162
|
+
ax.fill(theta, d, facecolor=color, alpha=0.25, label='_nolegend_')
|
|
163
|
+
|
|
164
|
+
ax.set_varlabels(labels)
|
|
165
|
+
ax.set_rgrids([0.2, 0.4, 0.6, 0.8])
|
|
166
|
+
ax.set(title=kwargs.get('title', ''))
|
|
167
|
+
|
|
168
|
+
# add legend relative to top-left plot
|
|
169
|
+
legend_labels = legend_labels or ('Factor 1', 'Factor 2', 'Factor 3', 'Factor 4', 'Factor 5')
|
|
170
|
+
legend = ax.legend(legend_labels, loc=(0.95, 0.95), labelspacing=0.1)
|
|
171
|
+
|
|
172
|
+
plt.show()
|
|
173
|
+
|
|
174
|
+
return fig, ax
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == '__main__':
|
|
178
|
+
data = [[0.88, 0.01, 0.03, 0.03, 0.00, 0.06, 0.01, 0.00],
|
|
179
|
+
[0.07, 0.95, 0.04, 0.05, 0.00, 0.02, 0.01, 0.00],
|
|
180
|
+
[0.01, 0.02, 0.85, 0.19, 0.05, 0.10, 0.00, 0.00],
|
|
181
|
+
[0.02, 0.01, 0.07, 0.01, 0.21, 0.12, 0.98, 0.00],
|
|
182
|
+
[0.01, 0.01, 0.02, 0.71, 0.74, 0.70, 0.30, 0.20]]
|
|
183
|
+
|
|
184
|
+
fig, ax = radar(data=data, labels=['Sulfate', 'Nitrate', 'EC', 'OC1', 'OC2', 'OP', 'CO', 'O3'], title='Basecase')
|
AeroViz/plot/scatter.py
CHANGED
|
@@ -22,9 +22,11 @@ def scatter(df: pd.DataFrame,
|
|
|
22
22
|
x: str,
|
|
23
23
|
y: str,
|
|
24
24
|
c: str | None = None,
|
|
25
|
+
color: str | None = '#7a97c9',
|
|
25
26
|
s: str | None = None,
|
|
26
27
|
cmap='jet',
|
|
27
28
|
regression=False,
|
|
29
|
+
regression_line_color: str | None = sns.xkcd_rgb["denim blue"],
|
|
28
30
|
diagonal=False,
|
|
29
31
|
ax: Axes | None = None,
|
|
30
32
|
**kwargs
|
|
@@ -41,6 +43,8 @@ def scatter(df: pd.DataFrame,
|
|
|
41
43
|
y : str
|
|
42
44
|
The column name for the y-axis values.
|
|
43
45
|
c : str, optional
|
|
46
|
+
The column name for c encoding. Default is None.
|
|
47
|
+
color : str, optional
|
|
44
48
|
The column name for color encoding. Default is None.
|
|
45
49
|
s : str, optional
|
|
46
50
|
The column name for size encoding. Default is None.
|
|
@@ -48,6 +52,8 @@ def scatter(df: pd.DataFrame,
|
|
|
48
52
|
The colormap to use for the color encoding. Default is 'jet'.
|
|
49
53
|
regression : bool, optional
|
|
50
54
|
If True, fits and plots a linear regression line. Default is False.
|
|
55
|
+
regression_line_color : str, optional
|
|
56
|
+
The color of the regression line. Default is 'sns.xkcd_rgb["denim blue"]'.
|
|
51
57
|
diagonal : bool, optional
|
|
52
58
|
If True, plots a 1:1 diagonal line. Default is False.
|
|
53
59
|
ax : Axes, optional
|
|
@@ -118,7 +124,7 @@ def scatter(df: pd.DataFrame,
|
|
|
118
124
|
x_data, y_data, s_data = df_[x].to_numpy(), df_[y].to_numpy(), df_[s].to_numpy()
|
|
119
125
|
check_empty(x_data, y_data, s_data)
|
|
120
126
|
|
|
121
|
-
scatter = ax.scatter(x_data, y_data, s=50 * (s_data / s_data.max()) ** 1.5, color=
|
|
127
|
+
scatter = ax.scatter(x_data, y_data, s=50 * (s_data / s_data.max()) ** 1.5, color=color, alpha=0.5,
|
|
122
128
|
edgecolors='white')
|
|
123
129
|
colorbar = False
|
|
124
130
|
|
|
@@ -135,7 +141,7 @@ def scatter(df: pd.DataFrame,
|
|
|
135
141
|
x_data, y_data = df_[x].to_numpy(), df_[y].to_numpy()
|
|
136
142
|
check_empty(x_data, y_data)
|
|
137
143
|
|
|
138
|
-
scatter = ax.scatter(x_data, y_data, s=30, color=
|
|
144
|
+
scatter = ax.scatter(x_data, y_data, s=30, color=color, alpha=0.5, edgecolors='white')
|
|
139
145
|
colorbar = False
|
|
140
146
|
|
|
141
147
|
ax.set(xlim=kwargs.get('xlim', (x_data.min(), x_data.max())),
|
|
@@ -144,21 +150,24 @@ def scatter(df: pd.DataFrame,
|
|
|
144
150
|
ylabel=kwargs.get('ylabel', Unit(y)),
|
|
145
151
|
title=kwargs.get('title', ''))
|
|
146
152
|
|
|
153
|
+
ax.xaxis.set_major_formatter(ScalarFormatter())
|
|
154
|
+
ax.yaxis.set_major_formatter(ScalarFormatter())
|
|
155
|
+
|
|
147
156
|
if colorbar:
|
|
148
157
|
plt.colorbar(scatter, extend='both', label=Unit(c))
|
|
149
158
|
|
|
150
159
|
if regression:
|
|
151
160
|
text, y_predict, slope = linear_regression_base(x_data, y_data)
|
|
152
|
-
ax.plot(x_data, y_predict, linewidth=3, color=
|
|
153
|
-
plt.text(0.05, 0.95, text, fontdict={'weight': 'bold'}, color=
|
|
161
|
+
ax.plot(x_data, y_predict, linewidth=3, color=regression_line_color, alpha=1, zorder=3)
|
|
162
|
+
plt.text(0.05, 0.95, text, fontdict={'weight': 'bold'}, color=regression_line_color,
|
|
154
163
|
ha='left', va='top', transform=ax.transAxes)
|
|
155
164
|
|
|
156
165
|
if diagonal:
|
|
157
166
|
ax.axline((0, 0), slope=1., color='k', lw=2, ls='--', alpha=0.5, label='1:1')
|
|
158
|
-
plt.text(0.91, 0.97, r'$\bf 1:1\ Line$', color='k', ha='right', va='top', transform=ax.transAxes)
|
|
159
167
|
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
data_range = min(ax.get_xlim()[1] - ax.get_xlim()[0], ax.get_ylim()[1] - ax.get_ylim()[0])
|
|
169
|
+
plt.text(0.9 * data_range, 0.9 * data_range, r'$\bf 1:1\ Line$', color='k', ha='left', va='bottom',
|
|
170
|
+
bbox=dict(facecolor='white', edgecolor='none', alpha=0.1, pad=3))
|
|
162
171
|
|
|
163
172
|
plt.show()
|
|
164
173
|
|
|
@@ -10,7 +10,7 @@ from AeroViz.plot.utils import *
|
|
|
10
10
|
__all__ = ['koschmieder']
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@set_figure
|
|
13
|
+
@set_figure(figsize=(2.4, 3))
|
|
14
14
|
def koschmieder(df: pd.DataFrame,
|
|
15
15
|
vis: str,
|
|
16
16
|
ext: list[str],
|
|
@@ -30,8 +30,8 @@ def koschmieder(df: pd.DataFrame,
|
|
|
30
30
|
|
|
31
31
|
fig, ax = plt.subplots(**kwargs.get('fig_kws', {})) if ax is None else (ax.get_figure(), ax)
|
|
32
32
|
|
|
33
|
-
boxcolors = ['#
|
|
34
|
-
scattercolor = ['
|
|
33
|
+
boxcolors = ['#a5bf6b', '#3f83bf']
|
|
34
|
+
scattercolor = ['green', 'blue']
|
|
35
35
|
arts = []
|
|
36
36
|
labels = []
|
|
37
37
|
|
|
@@ -74,15 +74,18 @@ def koschmieder(df: pd.DataFrame,
|
|
|
74
74
|
label=f'Vis (km) = {round(coeff)} / Ext')
|
|
75
75
|
|
|
76
76
|
arts.append(line)
|
|
77
|
-
|
|
77
|
+
if 'dry' in ext_col:
|
|
78
|
+
labels.append(f'Vis (km) = {round(coeff)} / Ext (dry)')
|
|
79
|
+
else:
|
|
80
|
+
labels.append(f'Vis (km) = {round(coeff)} / Ext (amb)')
|
|
78
81
|
|
|
79
82
|
ax.legend(handles=arts, labels=labels, loc='upper right', prop=dict(weight='bold'), bbox_to_anchor=(0.99, 0.99))
|
|
80
83
|
|
|
81
|
-
ax.set(xlabel=kwargs.get('
|
|
82
|
-
ylabel=kwargs.get('
|
|
83
|
-
title=kwargs.get('
|
|
84
|
+
ax.set(xlabel=kwargs.get('xlabel', 'Visibility (km)'),
|
|
85
|
+
ylabel=kwargs.get('ylabel', 'Extinction (1/Mm)'),
|
|
86
|
+
title=kwargs.get('title', 'Koschmieder relationship'),
|
|
84
87
|
xlim=kwargs.get('xlim', (0, 30)),
|
|
85
|
-
ylim=kwargs.get('ylim', (0,
|
|
88
|
+
ylim=kwargs.get('ylim', (0, 800))
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
plt.xticks(ticks=np.array(range(0, 31, 5)), labels=np.array(range(0, 31, 5)))
|
|
@@ -348,7 +348,6 @@ def timeseries_stacked(df,
|
|
|
348
348
|
ylabel=kwargs.get('ylabel', 'Percentage (%)'),
|
|
349
349
|
xlim=kwargs.get('xlim', (st_tm, fn_tm)),
|
|
350
350
|
ylim=(0, 100),
|
|
351
|
-
title=kwargs.get('title', '')
|
|
352
351
|
)
|
|
353
352
|
|
|
354
353
|
xticks = kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=major_freq))
|
|
@@ -1,104 +1,111 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
+
from pandas import Grouper, Timedelta
|
|
5
|
+
|
|
4
6
|
from AeroViz.rawDataReader.config.supported_instruments import meta
|
|
5
7
|
from AeroViz.rawDataReader.script import *
|
|
6
8
|
|
|
7
9
|
__all__ = ['RawDataReader']
|
|
8
10
|
|
|
11
|
+
SUPPORTED_INSTRUMENTS = [
|
|
12
|
+
NEPH, Aurora, SMPS, GRIMM, APS_3321, AE33, AE43, BC1054,
|
|
13
|
+
MA350, TEOM, OCEC, IGAC, VOC, EPA, Minion
|
|
14
|
+
]
|
|
15
|
+
|
|
9
16
|
|
|
10
17
|
def RawDataReader(instrument_name: str,
|
|
11
18
|
path: Path,
|
|
12
|
-
qc: bool = True,
|
|
13
|
-
csv_raw: bool = True,
|
|
14
19
|
reset: bool = False,
|
|
20
|
+
qc: bool | str = True,
|
|
21
|
+
qc_freq: str | None = None,
|
|
15
22
|
rate: bool = True,
|
|
16
23
|
append_data: bool = False,
|
|
17
|
-
start: datetime
|
|
18
|
-
end: datetime
|
|
19
|
-
mean_freq='1h',
|
|
20
|
-
csv_out=True,
|
|
24
|
+
start: datetime = None,
|
|
25
|
+
end: datetime = None,
|
|
26
|
+
mean_freq: str = '1h',
|
|
27
|
+
csv_out: bool = True,
|
|
21
28
|
):
|
|
22
29
|
"""
|
|
23
30
|
Factory function to instantiate the appropriate reader module for a given instrument and
|
|
24
31
|
return the processed data over the specified time range.
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
csv_out : bool, optional (default=True)
|
|
49
|
-
If True, output the processed data as a CSV file.
|
|
50
|
-
|
|
51
|
-
Return
|
|
52
|
-
------
|
|
53
|
-
reader_module : Reader
|
|
54
|
-
An instance of the reader module corresponding to the specified instrument, which processes
|
|
55
|
-
the data and returns it in a usable format.
|
|
56
|
-
|
|
57
|
-
Raises
|
|
58
|
-
------
|
|
59
|
-
ValueError
|
|
60
|
-
If the `instrument_name` provided is not a valid key in the `meta` dictionary.
|
|
61
|
-
|
|
62
|
-
Example
|
|
63
|
-
-------
|
|
33
|
+
:param instrument_name: The name of the instrument for which to read data. Must be a valid key in the `meta` dictionary.
|
|
34
|
+
:param path: The directory where raw data files for the instrument are stored.
|
|
35
|
+
:param reset: If True, reset the state and reprocess the data from scratch.
|
|
36
|
+
:param qc: If True, apply quality control (QC) to the raw data.
|
|
37
|
+
:param qc_freq: Frequency at which to perform QC. Must be one of 'W', 'M', 'Q', 'Y' for weekly, monthly, quarterly, or yearly.
|
|
38
|
+
:param rate: If True, calculate rates from the data.
|
|
39
|
+
:param append_data: If True, append new data to the existing dataset instead of overwriting it.
|
|
40
|
+
:param start: Start time for filtering the data. If None, no start time filtering will be applied.
|
|
41
|
+
:param end: End time for filtering the data. If None, no end time filtering will be applied.
|
|
42
|
+
:param mean_freq: Resampling frequency for averaging the data. Example: '1h' for hourly mean.
|
|
43
|
+
:param csv_out: If True, output the processed data as a CSV file.
|
|
44
|
+
|
|
45
|
+
:return: An instance of the reader module corresponding to the specified instrument, which processes the data and returns it in a usable format.
|
|
46
|
+
|
|
47
|
+
:raises ValueError: If the `instrument_name` provided is not a valid key in the `meta` dictionary.
|
|
48
|
+
:raises ValueError: If the specified path does not exist or is not a directory.
|
|
49
|
+
:raises ValueError: If the QC frequency is invalid.
|
|
50
|
+
:raises ValueError: If start and end times are not both provided or are invalid.
|
|
51
|
+
:raises ValueError: If the mean_freq is not a valid frequency string.
|
|
52
|
+
|
|
53
|
+
:Example:
|
|
54
|
+
|
|
64
55
|
To read and process data for the BC1054 instrument:
|
|
65
56
|
|
|
66
57
|
>>> from pathlib import Path
|
|
67
58
|
>>> from datetime import datetime
|
|
68
|
-
>>>
|
|
69
|
-
>>>
|
|
59
|
+
>>>
|
|
60
|
+
>>> data = RawDataReader(
|
|
61
|
+
... instrument_name='BC1054',
|
|
62
|
+
... path=Path('/path/to/data'),
|
|
63
|
+
... start=datetime(2024, 2, 1),
|
|
64
|
+
... end=datetime(2024, 7, 31, 23))
|
|
70
65
|
"""
|
|
71
66
|
# Mapping of instrument names to their respective classes
|
|
72
|
-
instrument_class_map = {
|
|
73
|
-
'NEPH': NEPH,
|
|
74
|
-
'Aurora': Aurora,
|
|
75
|
-
'SMPS': SMPS,
|
|
76
|
-
'GRIMM': GRIMM,
|
|
77
|
-
'APS_3321': APS_3321,
|
|
78
|
-
'AE33': AE33,
|
|
79
|
-
'AE43': AE43,
|
|
80
|
-
'BC1054': BC1054,
|
|
81
|
-
'MA350': MA350,
|
|
82
|
-
'TEOM': TEOM,
|
|
83
|
-
'OCEC': OCEC,
|
|
84
|
-
'IGAC': IGAC,
|
|
85
|
-
'VOC': VOC,
|
|
86
|
-
'Table': Table,
|
|
87
|
-
'EPA_vertical': EPA_vertical,
|
|
88
|
-
'Minion': Minion
|
|
89
|
-
# Add other instruments and their corresponding classes here
|
|
90
|
-
}
|
|
67
|
+
instrument_class_map = {cls.__name__.split('.')[-1]: cls for cls in SUPPORTED_INSTRUMENTS}
|
|
91
68
|
|
|
92
69
|
# Check if the instrument name is in the map
|
|
93
70
|
if instrument_name not in meta.keys():
|
|
94
71
|
raise ValueError(f"Instrument name '{instrument_name}' is not valid. \nMust be one of: {list(meta.keys())}")
|
|
95
72
|
|
|
73
|
+
# 檢查 path 是否存在且是一個目錄
|
|
74
|
+
if not isinstance(path, Path):
|
|
75
|
+
path = Path(path)
|
|
76
|
+
if not path.exists() or not path.is_dir():
|
|
77
|
+
raise ValueError(f"The specified path '{path}' does not exist or is not a directory.")
|
|
78
|
+
|
|
79
|
+
# Validate the QC frequency
|
|
80
|
+
if qc_freq is not None:
|
|
81
|
+
try:
|
|
82
|
+
Grouper(freq=qc_freq)
|
|
83
|
+
except ValueError as e:
|
|
84
|
+
raise ValueError(f"Invalid frequency: {qc_freq}. Error: {str(e)}")
|
|
85
|
+
except TypeError as e:
|
|
86
|
+
raise ValueError(f"Invalid frequency type: {qc_freq}. Frequency should be a string.")
|
|
87
|
+
|
|
88
|
+
if start and end:
|
|
89
|
+
if end.hour == 0 and end.minute == 0 and end.second == 0:
|
|
90
|
+
end = end.replace(hour=23)
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError("Both start and end times must be provided.")
|
|
93
|
+
if end <= start:
|
|
94
|
+
raise ValueError(f"Invalid time range: start {start} is after end {end}")
|
|
95
|
+
|
|
96
|
+
# 驗證 mean_freq 的格式是否正確
|
|
97
|
+
try:
|
|
98
|
+
Timedelta(mean_freq)
|
|
99
|
+
except ValueError:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Invalid mean_freq: '{mean_freq}'. It should be a valid frequency string (e.g., '1H', '30min', '1D').")
|
|
102
|
+
|
|
96
103
|
# Instantiate the class and return the instance
|
|
97
104
|
reader_module = instrument_class_map[instrument_name].Reader(
|
|
98
105
|
path=path,
|
|
99
|
-
qc=qc,
|
|
100
|
-
csv_raw=csv_raw,
|
|
101
106
|
reset=reset,
|
|
107
|
+
qc=qc,
|
|
108
|
+
qc_freq=qc_freq,
|
|
102
109
|
rate=rate,
|
|
103
110
|
append_data=append_data
|
|
104
111
|
)
|