eval-ac 0.3.1__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.
eval_ac/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ evaluate the absolute calibration with liquid nitrogen of an
3
+ Radiometer Physics GmbH (RPG) microwave radiometer HATPRO.
4
+ """
5
+
6
+ __version__ = "0.3.1"
7
+ __author__ = 'Andreas Foth'
eval_ac/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allows running eval_ac with ``python -m eval_ac``."""
2
+ import sys
3
+
4
+ from eval_ac.cli import main
5
+
6
+ sys.exit(main())
eval_ac/analysis.py ADDED
@@ -0,0 +1,326 @@
1
+ """Selection, quality control and drift analysis of the calibration history.
2
+
3
+ Typical usage::
4
+
5
+ from eval_ac.convert_abscal_his import read_abscal_his
6
+ from eval_ac.analysis import select_cal_type, calibration_drift, \
7
+ drift_exceedances
8
+
9
+ ds = select_cal_type(read_abscal_his('ABSCAL.HIS'))
10
+ drift = calibration_drift(ds)
11
+ for row in drift_exceedances(drift):
12
+ print(row)
13
+ """
14
+
15
+ import warnings
16
+
17
+ import numpy as np
18
+ import xarray as xr
19
+
20
+ from eval_ac.convert_abscal_his import TIME_REFERENCE
21
+
22
+ #: Calibration types stored in ``cal_type_1`` and ``cal_type_2``.
23
+ CAL_TYPE_NONE = 0
24
+ CAL_TYPE_LN2 = 1
25
+ CAL_TYPE_SKY_TIPPING = 2
26
+
27
+ #: Value of ``calibration_flag`` for channels that were not calibrated.
28
+ FLAG_NOT_CALIBRATED = 0
29
+
30
+ #: Recommended maximum interval between two absolute calibrations in days:
31
+ #: "every 5 to 6 months of operation or after transportation"
32
+ #: (RPG manual RPG-MWR-STD-SW, section 3.1.3).
33
+ MAX_CALIBRATION_AGE_DAYS = 183
34
+
35
+ #: Valid range of the non-linearity factor, 0.9 <= alpha < 1
36
+ #: (RPG manual RPG-MWR-STD-SW, section 3.1.3.1, equation 1).
37
+ ALPHA_RANGE = (0.9, 1.0)
38
+
39
+ #: Variables with one value per calibration entry and channel.
40
+ CHANNEL_VARIABLES = ('gain', 'temp_noise', 'temp_sys', 'alpha')
41
+
42
+ #: Default number of calibrations before the latest one that form the
43
+ #: reference of the drift analysis.
44
+ DEFAULT_N_REFERENCE = 5
45
+
46
+ #: Default thresholds of the drift analysis in percent. They are about the
47
+ #: 99th percentile of the deviations found in the example data of a HATPRO
48
+ #: (2018-2026) and should be adapted to the instrument.
49
+ DEFAULT_THRESHOLDS = {
50
+ 'gain': 10.0,
51
+ 'temp_noise': 2.5,
52
+ 'temp_sys': 2.5,
53
+ 'alpha': 0.5,
54
+ }
55
+
56
+
57
+ def receiver_masks(data_set: xr.Dataset) -> dict:
58
+ """Returns a boolean channel mask for each receiver number."""
59
+ if 'receiver' not in data_set.coords:
60
+ return {1: np.ones(data_set.sizes['freq'], dtype=bool)}
61
+ receiver = data_set['receiver'].values
62
+ return {int(number): receiver == number for number in np.unique(receiver)}
63
+
64
+
65
+ def channel_cal_type(data_set: xr.Dataset) -> xr.DataArray:
66
+ """Returns the calibration type of each entry and channel.
67
+
68
+ Channels of receiver 1 get ``cal_type_1``, channels of receiver 2 get
69
+ ``cal_type_2``.
70
+ """
71
+ cal_type = np.empty((data_set.sizes['n_samples'],
72
+ data_set.sizes['freq']), dtype=np.int32)
73
+ for number, mask in receiver_masks(data_set).items():
74
+ cal_type[:, mask] = data_set[f'cal_type_{number}'].values[:, None]
75
+ return xr.DataArray(cal_type, dims=('n_samples', 'freq'))
76
+
77
+
78
+ def select_cal_type(data_set: xr.Dataset,
79
+ cal_type: int = CAL_TYPE_LN2) -> xr.Dataset:
80
+ """Keeps only the calibrations of the given type.
81
+
82
+ Entries in which no receiver has the requested calibration type are
83
+ removed. If only one receiver of an entry has the requested type, the
84
+ values of the other receiver are set to NaN.
85
+
86
+ Args:
87
+ data_set: Dataset as returned by
88
+ :func:`eval_ac.convert_abscal_his.read_abscal_his`.
89
+ cal_type: Calibration type to keep (default: liquid nitrogen).
90
+
91
+ Returns:
92
+ Dataset with the selected calibrations.
93
+ """
94
+ valid = channel_cal_type(data_set) == cal_type
95
+ keep = valid.any('freq').values
96
+ selected = data_set.isel(n_samples=keep)
97
+ valid = valid.isel(n_samples=keep)
98
+ for var in CHANNEL_VARIABLES:
99
+ if var in selected:
100
+ selected[var] = selected[var].where(valid.values,
101
+ drop=False)
102
+ selected[var].attrs = data_set[var].attrs
103
+ return selected
104
+
105
+
106
+ def not_calibrated(data_set: xr.Dataset) -> np.ndarray:
107
+ """Returns True for every entry and channel that was not calibrated."""
108
+ if 'calibration_flag' not in data_set:
109
+ return np.zeros((data_set.sizes['n_samples'],
110
+ data_set.sizes['freq']), dtype=bool)
111
+ return data_set['calibration_flag'].values == FLAG_NOT_CALIBRATED
112
+
113
+
114
+ def valid_samples(values: np.ndarray) -> np.ndarray:
115
+ """Returns the indices of the entries with at least one value."""
116
+ return np.flatnonzero(~np.all(np.isnan(values), axis=1))
117
+
118
+
119
+ def _receiver_drift(values, flagged, n_reference):
120
+ """Drift of one variable and one receiver.
121
+
122
+ Returns:
123
+ Tuple ``(reference, deviation, latest, n_used)``.
124
+ """
125
+ # the latest entry is defined without the flags, so that a latest
126
+ # calibration without any calibrated channel is not silently skipped
127
+ samples = valid_samples(values)
128
+ values = np.where(flagged, np.nan, values)
129
+ n_channels = values.shape[1]
130
+ if samples.size == 0:
131
+ return (np.full(n_channels, np.nan), np.full(values.shape, np.nan),
132
+ -1, np.zeros(n_channels, dtype=np.int32))
133
+ latest = samples[-1]
134
+ reference_values = values[samples[-1 - n_reference:-1]]
135
+ with warnings.catch_warnings():
136
+ warnings.simplefilter('ignore', RuntimeWarning) # all-NaN channels
137
+ reference = np.nanmedian(reference_values, axis=0) \
138
+ if reference_values.size else np.full(n_channels, np.nan)
139
+ deviation = 100 * (values - reference) / reference
140
+ n_used = np.sum(~np.isnan(reference_values), axis=0).astype(np.int32)
141
+ return reference, deviation, latest, n_used
142
+
143
+
144
+ def calibration_drift(data_set: xr.Dataset,
145
+ variables=CHANNEL_VARIABLES,
146
+ n_reference: int = DEFAULT_N_REFERENCE) -> xr.Dataset:
147
+ """Compares each calibration with the median of the previous ones.
148
+
149
+ For each receiver, the reference is the median of the ``n_reference``
150
+ calibrations before the latest one. Channels that were not calibrated
151
+ (``calibration_flag == 0``) are neither used for the reference nor
152
+ evaluated.
153
+
154
+ Args:
155
+ data_set: Dataset, typically after :func:`select_cal_type`.
156
+ variables: Variables to analyse.
157
+ n_reference: Number of calibrations forming the reference.
158
+
159
+ Returns:
160
+ :class:`xarray.Dataset` with, for each analysed variable ``var``,
161
+ ``var_reference`` (freq) with the reference value and
162
+ ``var_deviation`` (n_samples, freq) with the deviation of every entry
163
+ from the reference in percent. In addition, ``latest_sample`` (freq)
164
+ holds the position of the latest entry of each channel and
165
+ ``n_reference_used`` (freq) the number of values in the reference.
166
+ """
167
+ flagged = not_calibrated(data_set)
168
+ n_samples, n_freq = data_set.sizes['n_samples'], data_set.sizes['freq']
169
+ latest = np.full(n_freq, -1, dtype=np.int32)
170
+ n_used = np.zeros(n_freq, dtype=np.int32)
171
+ drift = xr.Dataset(coords={name: data_set.coords[name]
172
+ for name in data_set.coords})
173
+
174
+ for var in variables:
175
+ reference = np.full(n_freq, np.nan)
176
+ deviation = np.full((n_samples, n_freq), np.nan)
177
+ for mask in receiver_masks(data_set).values():
178
+ (reference[mask], deviation[:, mask], latest[mask],
179
+ n_used[mask]) = _receiver_drift(
180
+ data_set[var].values[:, mask].astype(float),
181
+ flagged[:, mask], n_reference)
182
+ long_name = data_set[var].attrs.get('long_name', var)
183
+ drift[f'{var}_reference'] = ('freq', reference, {
184
+ 'long_name': f'{long_name}, median of {n_reference} previous '
185
+ 'calibrations',
186
+ 'units': data_set[var].attrs.get('units', '')})
187
+ drift[f'{var}_deviation'] = (('n_samples', 'freq'), deviation, {
188
+ 'long_name': f'deviation of {long_name} from reference',
189
+ 'units': '%'})
190
+
191
+ drift['latest_sample'] = ('freq', latest, {
192
+ 'long_name': 'position of the latest calibration along n_samples'})
193
+ drift['n_reference_used'] = ('freq', n_used, {
194
+ 'long_name': 'number of calibrations in the reference'})
195
+ drift.attrs['n_reference'] = n_reference
196
+ return drift
197
+
198
+
199
+ def _latest_per_receiver(data_set: xr.Dataset, variable='gain'):
200
+ """Yields ``(receiver number, channel mask, position of latest entry)``.
201
+
202
+ Receivers without any entry are skipped.
203
+ """
204
+ for number, mask in receiver_masks(data_set).items():
205
+ samples = valid_samples(data_set[variable].values[:, mask]
206
+ .astype(float))
207
+ if samples.size:
208
+ yield number, mask, samples[-1]
209
+
210
+
211
+ def latest_not_calibrated(data_set: xr.Dataset, variable='gain') -> list:
212
+ """Lists the channels of the latest calibration with flag 0.
213
+
214
+ The latest calibration is determined per receiver.
215
+
216
+ Returns:
217
+ List of dictionaries with the keys ``freq`` and ``receiver``.
218
+ """
219
+ flagged = not_calibrated(data_set)
220
+ rows = []
221
+ for number, mask, latest in _latest_per_receiver(data_set, variable):
222
+ for freq in data_set['freq'].values[mask][flagged[latest, mask]]:
223
+ rows.append({'freq': float(freq), 'receiver': number})
224
+ return rows
225
+
226
+
227
+ def receiver_time(data_set: xr.Dataset, number: int) -> np.ndarray:
228
+ """Returns the calibration times of receiver ``number``.
229
+
230
+ Uses ``time_of_rec_<number>`` (seconds since 2001-01-01, or already
231
+ decoded when read from a NetCDF file) and falls back to the coordinate
232
+ ``time``.
233
+ """
234
+ name = f'time_of_rec_{number}'
235
+ if name not in data_set:
236
+ return data_set['time'].values
237
+ values = data_set[name].values
238
+ if np.issubdtype(values.dtype, np.datetime64):
239
+ return values
240
+ return TIME_REFERENCE + values.astype('timedelta64[s]')
241
+
242
+
243
+ def calibration_age(data_set: xr.Dataset, now=None) -> list:
244
+ """Returns the age of the latest calibration of each receiver.
245
+
246
+ Args:
247
+ data_set: Dataset, typically after :func:`select_cal_type`.
248
+ now: Reference time (anything accepted by ``numpy.datetime64``,
249
+ default: current UTC time).
250
+
251
+ Returns:
252
+ List of dictionaries with the keys ``receiver``, ``time``
253
+ (``numpy.datetime64``) and ``age_days``.
254
+ """
255
+ now = np.datetime64('now' if now is None else now, 's')
256
+ rows = []
257
+ for number, _, latest in _latest_per_receiver(data_set):
258
+ time = receiver_time(data_set, number)[latest].astype(
259
+ 'datetime64[s]')
260
+ rows.append({'receiver': number, 'time': time,
261
+ 'age_days': float((now - time) / np.timedelta64(1, 'D'))})
262
+ return rows
263
+
264
+
265
+ def alpha_out_of_range(data_set: xr.Dataset, valid_range=ALPHA_RANGE) -> list:
266
+ """Lists channels of the latest calibration with alpha out of range.
267
+
268
+ Channels that were not calibrated (flag 0) are not checked.
269
+
270
+ Args:
271
+ data_set: Dataset, typically after :func:`select_cal_type`.
272
+ valid_range: Tuple ``(low, high)``; valid is ``low <= alpha < high``.
273
+
274
+ Returns:
275
+ List of dictionaries with the keys ``freq``, ``receiver`` and
276
+ ``alpha``.
277
+ """
278
+ low, high = valid_range
279
+ flagged = not_calibrated(data_set)
280
+ rows = []
281
+ for number, mask, latest in _latest_per_receiver(data_set, 'alpha'):
282
+ alpha = data_set['alpha'].values[latest, mask].astype(float)
283
+ with np.errstate(invalid='ignore'):
284
+ bad = ~((alpha >= low) & (alpha < high))
285
+ bad &= ~flagged[latest, mask] & ~np.isnan(alpha)
286
+ for freq, value in zip(data_set['freq'].values[mask][bad],
287
+ alpha[bad]):
288
+ rows.append({'freq': float(freq), 'receiver': number,
289
+ 'alpha': float(value)})
290
+ return rows
291
+
292
+
293
+ def drift_exceedances(drift: xr.Dataset, thresholds=None) -> list:
294
+ """Lists the channels of the latest calibration exceeding a threshold.
295
+
296
+ Args:
297
+ drift: Dataset as returned by :func:`calibration_drift`.
298
+ thresholds: Dictionary ``{variable: threshold in percent}``. Missing
299
+ variables use :data:`DEFAULT_THRESHOLDS`.
300
+
301
+ Returns:
302
+ List of dictionaries with the keys ``variable``, ``freq``,
303
+ ``receiver``, ``deviation`` and ``threshold``, sorted by variable
304
+ and frequency.
305
+ """
306
+ thresholds = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
307
+ latest = drift['latest_sample'].values
308
+ rows = []
309
+ for var, threshold in thresholds.items():
310
+ if f'{var}_deviation' not in drift:
311
+ continue
312
+ deviation = drift[f'{var}_deviation'].values
313
+ for channel, sample in enumerate(latest):
314
+ if sample < 0:
315
+ continue
316
+ value = deviation[sample, channel]
317
+ if abs(value) > threshold: # NaN never exceeds
318
+ rows.append({
319
+ 'variable': var,
320
+ 'freq': float(drift['freq'].values[channel]),
321
+ 'receiver': int(drift['receiver'].values[channel])
322
+ if 'receiver' in drift.coords else 1,
323
+ 'deviation': float(value),
324
+ 'threshold': float(threshold),
325
+ })
326
+ return rows
eval_ac/cli.py ADDED
@@ -0,0 +1,234 @@
1
+ """Command line interface of eval_ac.
2
+
3
+ Example::
4
+
5
+ eval-ac ABSCAL.HIS --netcdf abscal.nc --plot results_ln2_cal.png
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+
11
+ from eval_ac import __version__
12
+
13
+ #: Exit code if the quality report contains a warning and
14
+ #: ``--fail-on-warning`` is given.
15
+ EXIT_WARNING = 2
16
+
17
+
18
+ def _threshold(text: str) -> tuple:
19
+ """Parses 'VARIABLE=PERCENT'."""
20
+ # pylint: disable=import-outside-toplevel
21
+ from eval_ac.analysis import DEFAULT_THRESHOLDS
22
+ try:
23
+ name, value = text.split('=')
24
+ value = float(value)
25
+ except ValueError as error:
26
+ raise argparse.ArgumentTypeError(
27
+ f'expected VARIABLE=PERCENT, got {text!r}') from error
28
+ if name not in DEFAULT_THRESHOLDS:
29
+ raise argparse.ArgumentTypeError(
30
+ f'unknown variable {name!r}, choose from '
31
+ f'{", ".join(DEFAULT_THRESHOLDS)}')
32
+ return name, value
33
+
34
+
35
+ def build_parser() -> argparse.ArgumentParser:
36
+ """Creates the argument parser."""
37
+ parser = argparse.ArgumentParser(
38
+ prog='eval-ac',
39
+ description='Read the ABSCAL.HIS file of an RPG microwave radiometer '
40
+ '(e.g. HATPRO) and plot the history of the absolute '
41
+ 'calibration with liquid nitrogen.')
42
+ parser.add_argument('input', help='path of the ABSCAL.HIS file')
43
+ parser.add_argument('-n', '--netcdf', metavar='FILE',
44
+ help='write the converted data (all calibration '
45
+ 'types) to this NetCDF file')
46
+ parser.add_argument('-p', '--plot', metavar='FILE',
47
+ default='results_ln2_cal.png',
48
+ help='save the history plot to this file '
49
+ '(default: %(default)s)')
50
+ parser.add_argument('-d', '--drift-plot', metavar='FILE',
51
+ default='results_ln2_drift.png',
52
+ help='save the drift plot to this file '
53
+ '(default: %(default)s)')
54
+ parser.add_argument('--no-plot', action='store_true',
55
+ help='do not create any plot')
56
+ parser.add_argument('--show', action='store_true',
57
+ help='show the plots in a window')
58
+ parser.add_argument('--all-cal-types', action='store_true',
59
+ help='use all calibrations instead of only those '
60
+ 'with liquid nitrogen')
61
+
62
+ drift = parser.add_argument_group('drift analysis')
63
+ drift.add_argument('--n-reference', type=int, default=5, metavar='N',
64
+ help='number of calibrations before the latest one '
65
+ 'forming the reference (default: %(default)s)')
66
+ drift.add_argument('-t', '--threshold', type=_threshold, action='append',
67
+ default=[], metavar='VARIABLE=PERCENT',
68
+ help='drift threshold in percent, e.g. gain=8; can be '
69
+ 'given several times (defaults: gain=10, '
70
+ 'temp_noise=2.5, temp_sys=2.5, alpha=0.5)')
71
+
72
+ checks = parser.add_argument_group('quality checks')
73
+ checks.add_argument('--max-age', type=float, default=183, metavar='DAYS',
74
+ help='warn if the latest calibration is older '
75
+ '(default: %(default)s days; RPG recommends an '
76
+ 'absolute calibration every 5 to 6 months)')
77
+ checks.add_argument('--fail-on-warning', action='store_true',
78
+ help=f'exit with code {EXIT_WARNING} if the report '
79
+ 'contains a warning (drift, age, flag 0, alpha '
80
+ 'out of range)')
81
+
82
+ parser.add_argument('--version', action='version',
83
+ version=f'%(prog)s {__version__}')
84
+ return parser
85
+
86
+
87
+ def _report_age(data_set, max_age):
88
+ """Prints the date of the latest calibration; returns True on warning."""
89
+ # pylint: disable=import-outside-toplevel
90
+ from eval_ac.analysis import calibration_age
91
+ warned = False
92
+ for row in calibration_age(data_set):
93
+ date = str(row['time'].astype('datetime64[D]'))
94
+ line = (f'receiver {row["receiver"]}: latest calibration {date} '
95
+ f'({row["age_days"]:.0f} days ago)')
96
+ if row['age_days'] > max_age:
97
+ print(f'warning: {line}, older than {max_age:g} days')
98
+ warned = True
99
+ else:
100
+ print(line)
101
+ return warned
102
+
103
+
104
+ def _report_channels(data_set, rows, message, value=None):
105
+ """Prints a list of channels; returns True if there are any."""
106
+ if not rows:
107
+ return False
108
+ units = data_set['freq'].attrs.get('units', '')
109
+ print(f'warning: {len(rows)} channel(s) of the latest calibration '
110
+ f'{message}:')
111
+ for row in rows:
112
+ extra = f' {value(row)}' if value else ''
113
+ print(f' {row["freq"]:7.2f} {units} receiver {row["receiver"]}'
114
+ f'{extra}')
115
+ return True
116
+
117
+
118
+ def _report(data_set, drift, args) -> bool:
119
+ """Prints the quality report of the latest calibration.
120
+
121
+ Returns:
122
+ True if the report contains a warning.
123
+ """
124
+ # pylint: disable=import-outside-toplevel
125
+ from eval_ac import analysis
126
+
127
+ warnings = [
128
+ _report_age(data_set, args.max_age),
129
+ _report_channels(data_set, analysis.latest_not_calibrated(data_set),
130
+ 'were not calibrated (flag 0)'),
131
+ _report_channels(
132
+ data_set, analysis.alpha_out_of_range(data_set),
133
+ 'have a non-linearity factor outside 0.9 <= alpha < 1',
134
+ lambda row: f'alpha = {row["alpha"]:.4f}'),
135
+ ]
136
+ exceedances = analysis.drift_exceedances(drift, dict(args.threshold))
137
+ if exceedances:
138
+ warnings.append(_report_channels(
139
+ data_set, exceedances, 'exceed the drift threshold',
140
+ lambda row: f'{row["variable"]:<11} {row["deviation"]:+7.2f} % '
141
+ f'(threshold ±{row["threshold"]:g} %)'))
142
+ else:
143
+ print('drift: all channels of the latest calibration are within '
144
+ 'the thresholds')
145
+ return any(warnings)
146
+
147
+
148
+ def _load(args):
149
+ """Reads the file and selects the calibration type.
150
+
151
+ Returns:
152
+ Dataset, or None after printing an error message.
153
+ """
154
+ # pylint: disable=import-outside-toplevel
155
+ from eval_ac import analysis
156
+ from eval_ac.convert_abscal_his import read_abscal_his, write_netcdf
157
+
158
+ try:
159
+ data_set = read_abscal_his(args.input)
160
+ except (OSError, ValueError) as error:
161
+ print(f'eval-ac: error: {error}', file=sys.stderr)
162
+ return None
163
+ print(f'read {data_set.sizes["n_samples"]} calibration entries '
164
+ f'from {args.input}')
165
+
166
+ if args.netcdf:
167
+ write_netcdf(data_set, args.netcdf)
168
+ print(f'wrote {args.netcdf}')
169
+
170
+ if not args.all_cal_types:
171
+ data_set = analysis.select_cal_type(data_set, analysis.CAL_TYPE_LN2)
172
+ print(f'{data_set.sizes["n_samples"]} of them are calibrations with '
173
+ 'liquid nitrogen')
174
+ if data_set.sizes['n_samples'] == 0:
175
+ print('eval-ac: error: no calibrations with liquid nitrogen '
176
+ 'found (use --all-cal-types)', file=sys.stderr)
177
+ return None
178
+ return data_set
179
+
180
+
181
+ def _save_plots(args, data_set, drift, thresholds):
182
+ """Creates, saves and optionally shows the plots."""
183
+ # pylint: disable=import-outside-toplevel
184
+ import matplotlib.pyplot as plt
185
+ from eval_ac.plotting import plot_calibration_history, plot_drift
186
+
187
+ figures = [
188
+ (plot_calibration_history(data_set), args.plot),
189
+ (plot_drift(drift, thresholds=thresholds), args.drift_plot),
190
+ ]
191
+ for figure, filename in figures:
192
+ figure.savefig(filename)
193
+ print(f'wrote {filename}')
194
+ if args.show:
195
+ plt.show()
196
+ for figure, _ in figures:
197
+ plt.close(figure)
198
+
199
+
200
+ def main(argv=None) -> int:
201
+ """Runs the command line interface.
202
+
203
+ Args:
204
+ argv: Command line arguments (default: ``sys.argv[1:]``).
205
+
206
+ Returns:
207
+ Exit code (0 on success, 1 on error, 2 on a warning with
208
+ ``--fail-on-warning``).
209
+ """
210
+ args = build_parser().parse_args(argv)
211
+
212
+ # pylint: disable=import-outside-toplevel
213
+ import matplotlib
214
+ if not args.show:
215
+ matplotlib.use('Agg') # no display needed, e.g. on servers
216
+ from eval_ac import analysis
217
+
218
+ data_set = _load(args)
219
+ if data_set is None:
220
+ return 1
221
+
222
+ drift = analysis.calibration_drift(data_set, n_reference=args.n_reference)
223
+ warned = _report(data_set, drift, args)
224
+
225
+ if not args.no_plot:
226
+ _save_plots(args, data_set, drift, dict(args.threshold))
227
+
228
+ if args.fail_on_warning and warned:
229
+ return EXIT_WARNING
230
+ return 0
231
+
232
+
233
+ if __name__ == '__main__':
234
+ sys.exit(main())