scia_post_process_l2.process_orbit()   F
last analyzed

Complexity

Conditions 15

Size

Total Lines 205
Code Lines 136

Duplication

Lines 21
Ratio 10.24 %

Importance

Changes 0
Metric Value
cc 15
eloc 136
nop 5
dl 21
loc 205
rs 2.0999
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like scia_post_process_l2.process_orbit() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# vim: set fileencoding=utf-8
3
"""SCIAMACHY level 2 data post processing
4
5
Main script for SCIAMACHY orbital retrieval post processing
6
and data combining (to netcdf).
7
8
Copyright (c) 2018 Stefan Bender
9
10
This file is part of sciapy.
11
sciapy is free software: you can redistribute it or modify it
12
under the terms of the GNU General Public License as published by
13
the Free Software Foundation, version 2.
14
See accompanying LICENSE file or http://www.gnu.org/licenses/gpl-2.0.html.
15
"""
16
17
from __future__ import absolute_import, division, print_function
18
19
import glob
20
import os
21
import argparse as ap
22
import datetime as dt
23
import logging
24
from pkg_resources import resource_filename
25
26
import numpy as np
27
import pandas as pd
28
import xarray as xr
29
from scipy.interpolate import interp1d
30
31
from astropy import units
32
from astropy.time import Time
33
from astropy.utils import iers
34
import astropy.coordinates as coord
35
36
import sciapy.level1c as sl
37
from sciapy.level2 import density_pp as sd
38
from sciapy.level2 import scia_akm as sa
39
from sciapy.level2.igrf import gmag_igrf
40
from sciapy.level2.aacgm2005 import gmag_aacgm2005
41
from sciapy import __version__
42
try:
43
	from nrlmsise00 import msise_flat as msise
44
except ImportError:
45
	msise = None
46
try:
47
	from sciapy.level2.noem import noem_cpp
48
except ImportError:
49
	noem_cpp = None
50
51
F107_FILE = resource_filename("sciapy", "data/indices/f107_noontime_flux_obs.txt")
52
F107A_FILE = resource_filename("sciapy", "data/indices/f107a_noontime_flux_obs.txt")
53
AP_FILE = resource_filename("sciapy", "data/indices/spidr_ap_2000-2012.dat")
54
F107_ADJ_FILE = resource_filename("sciapy", "data/indices/spidr_f107_2000-2012.dat")
55
KP_FILE = resource_filename("sciapy", "data/indices/spidr_kp_2000-2012.dat")
56
57
PHI_FAC = 11.91
58
LST_FAC = -0.62
59
60
iers.conf.auto_download = False
61
iers.conf.auto_max_age = None
62
# Use a mirror page for the astrpy time data, see
63
# https://github.com/mzechmeister/serval/issues/33#issuecomment-551156361
64
# and
65
# https://github.com/astropy/astropy/issues/8981#issuecomment-523984247
66
iers.conf.iers_auto_url = "https://astroconda.org/aux/astropy_mirror/iers_a_1/finals2000A.all"
67
68
69 View Code Duplication
def solar_zenith_angle(alt, lat, lon, time):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
70
	atime = Time(time)
71
	loc = coord.EarthLocation.from_geodetic(
72
		height=alt * units.km,
73
		lat=lat * units.deg,
74
		lon=lon * units.deg,
75
	)
76
	altaz = coord.AltAz(location=loc, obstime=atime)
77
	sun = coord.get_sun(atime)
78
	return sun.transform_to(altaz).zen.value
79
80
81 View Code Duplication
def read_spectra(year, orbit, spec_base=None, skip_upleg=True):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
82
	"""Read and examine SCIAMACHY orbit spectra
83
84
	Reads the limb spactra and extracts the dates, times, latitudes,
85
	longitudes to be used to re-assess the retrieved geolocations.
86
87
	Parameters
88
	----------
89
	year: int
90
		The measurement year to select the corresponding subdir
91
		below `spec_base` (see below).
92
	orbit: int
93
		SCIAMACHY/Envisat orbit number of the spectra.
94
	spec_base: str, optional
95
		The root path to the level 1c spectra. Uses the current
96
		dir if not set or set to `None` (default).
97
	skip_upleg: bool, optional
98
		Skip upleg limb scans, i.e. night time scans. For NO retrievals,
99
		those are not used and should be not used here.
100
		Default: True
101
102
	Returns
103
	-------
104
	(dts, times, lats, lons, mlsts, alsts, eotcorr)
105
	"""
106
	fail = (None,) * 7
107
	if spec_base is None:
108
		spec_base = os.curdir
109
	spec_path = os.path.join(spec_base, "{0}".format(year))
110
	spec_path2 = os.path.join(spec_base, "{0}".format(int(year) + 1))
111
	logging.debug("spec_path: %s", spec_path)
112
	logging.debug("spec_path2: %s", spec_path)
113
	if not (os.path.isdir(spec_path) or os.path.isdir(spec_path2)):
114
		return fail
115
116
	# the star stands for the (optional) date subdir
117
	# to find all spectra for the orbit
118
	spfiles = glob.glob(
119
			'{0}/*/SCIA_limb_*_1_0_{1:05d}.dat.l_mpl_binary'
120
			.format(spec_path, orbit))
121
	# sometimes for whatever reason the orbit ends up in the wrong year subdir
122
	# looks in the subdir for the following year as well.
123
	spfiles += glob.glob(
124
			'{0}/*/SCIA_limb_*_1_0_{1:05d}.dat.l_mpl_binary'
125
			.format(spec_path2, orbit))
126
	if len(spfiles) < 2:
127
		return fail
128
129
	dts = []
130
	times = []
131
	lats = []
132
	lons = []
133
	mlsts = []
134
	alsts = []
135
136
	sls = sl.scia_limb_scan()
137
	for f in sorted(spfiles):
138
		sls.read_from_file(f)
139
		# copy the values from the l1c file
140
		lat, lon = sls.cent_lat_lon[:2]
141
		mlst, alst, eotcorr = sls.local_solar_time(False)
142
		tp_lats = sls.limb_data.tp_lat
143
		date = sls.date
144
		# debug output if requested
145
		logging.debug("file: %s", f)
146
		logging.debug("lat: %s, lon: %s", lat, lon)
147
		logging.debug("mlst: %s, alst: %s, eotcorr: %s", mlst, alst, eotcorr)
148
		logging.debug("tp_lats: %s", tp_lats)
149
		logging.debug("date: %s", date)
150
		if skip_upleg and ((tp_lats[1] - tp_lats[-2]) < 0.5):
151
			# Exclude non-downleg measurements where the latitude
152
			# of the last real tangent point (the last is dark sky)
153
			# is larger than or too close to the first latitude.
154
			# Requires an (empirical) separation of +0.5 degree.
155
			logging.debug("excluding upleg point at: %s, %s", lat, lon)
156
			continue
157
		dtdate = pd.to_datetime(dt.datetime(*date), utc=True)
158
		time_hour = dtdate.hour + dtdate.minute / 60.0 + dtdate.second / 3600.0
159
		logging.debug("mean lst: %s, apparent lst: %s, EoT: %s", mlst, alst, eotcorr)
160
		dts.append(dtdate)
161
		times.append(time_hour)
162
		lats.append(lat)
163
		lons.append(lon)
164
		mlsts.append(mlst)
165
		alsts.append(alst)
166
167
	if len(lats) < 2:
168
		# interpolation will fail with less than 2 points
169
		return fail
170
171
	return (np.asarray(dts),
172
			np.asarray(times),
173
			np.asarray(lats),
174
			np.asarray(lons),
175
			np.asarray(mlsts),
176
			np.asarray(alsts), eotcorr)
0 ignored issues
show
introduced by
The variable eotcorr does not seem to be defined in case the for loop on line 137 is not entered. Are you sure this can never be the case?
Loading history...
177
178
179
class _circ_interp(object):
180
	"""Interpolation on a circle"""
181
	def __init__(self, x, y, **kw):
182
		self.c_intpf = interp1d(x, np.cos(y), **kw)
183
		self.s_intpf = interp1d(x, np.sin(y), **kw)
184
185
	def __call__(self, x):
186
		return np.arctan2(self.s_intpf(x), self.c_intpf(x))
187
188
189
def process_orbit(
190
	orbit,
191
	ref_date="2000-01-01",
192
	dens_path=None,
193
	spec_base=None,
194
	use_msis=True,
195
):
196
	"""Post process retrieved SCIAMACHY orbit
197
198
	Parameters
199
	----------
200
	orbit: int
201
		SCIAMACHY/Envisat orbit number of the results to process.
202
	ref_date: str, optional
203
		Base date to calculate the relative days from,
204
		of the format "%Y-%m-%d". Default: 2000-01-01
205
	dens_path: str, optional
206
		The path to the level 2 data. Uses the current
207
		dir if not set or set to `None` (default).
208
	spec_base: str, optional
209
		The root path to the level 1c spectra. Uses the current
210
		dir if not set or set to `None` (default).
211
212
	Returns
213
	-------
214
	(dts0, time0, lst0, lon0, sdd): tuple
215
		dts0 - days since ref_date at equator crossing (float)
216
		time0 - utc hour into the day at equator crossing (float)
217
		lst0 - apparent local solar time at the equator (float)
218
		lon0 - longitude of the equator crossing (float)
219
		sdd - `scia_density_pp` instance of the post-processed data
220
	"""
221
	def _read_gm(fname):
222
		return dict(np.genfromtxt(fname, usecols=[0, 2], dtype=None))
223
224
	fail = (None,) * 5
225
	logging.debug("processing orbit: %s", orbit)
226
	dtrefdate = pd.to_datetime(ref_date, format="%Y-%m-%d", utc=True)
227
228
	dfiles = glob.glob(
229
			"{0}/000NO_orbit_{1:05d}_*_Dichten.txt"
230
			.format(dens_path, orbit))
231
	if len(dfiles) < 1:
232
		return fail
233
	logging.debug("dfiles: %s", dfiles)
234
	logging.debug("splits: %s", [fn.split('/') for fn in dfiles])
235
	ddict = dict([(fn, (fn.split('/')[-3:-1] + fn.split('/')[-1].split('_')[3:4]))
236
				for fn in dfiles])
237
	logging.debug("ddict: %s", ddict)
238
	year = ddict[sorted(ddict.keys())[0]][-1][:4]
239
	logging.debug("year: %s", year)
240
241
	dts, times, lats, lons, mlsts, alsts, eotcorr = \
242
		read_spectra(year, orbit, spec_base)
243
	if dts is None:
244
		# return early if reading the spectra failed
245
		return fail
246
247
	dts = pd.to_datetime(dts, utc=True) - dtrefdate
248
	dts = np.array([dtd.days + dtd.seconds / 86400. for dtd in dts])
249
	logging.debug("lats: %s, lons: %s, times: %s", lats, lons, times)
250
251
	sdd = sd.scia_densities_pp(ref_date=ref_date)
252
	sdd.read_from_file(dfiles[0])
253
	logging.debug("density lats: %s, lons: %s", sdd.lats, sdd.lons)
254
255
	# Re-interpolates the location (longitude) and times from the
256
	# limb scan spectra files along the orbit to determine the values
257
	# at the Equator and to fill in possibly missing data.
258
	#
259
	# y values are unit circle angles in radians (0 < phi < 2 pi or -pi < phi < pi)
260
	# longitudes
261
	lons_intpf = _circ_interp(
262
		lats[::-1], np.radians(lons[::-1]),
263
		fill_value="extrapolate",
264
	)
265
	# apparent local solar time (EoT corrected)
266
	lst_intpf = _circ_interp(
267
		lats[::-1], np.pi / 12. * alsts[::-1],
268
		fill_value="extrapolate",
269
	)
270
	# mean local solar time
271
	mst_intpf = _circ_interp(
272
		lats[::-1], np.pi / 12. * mlsts[::-1],
273
		fill_value="extrapolate",
274
	)
275
	# utc time (day)
276
	time_intpf = _circ_interp(
277
		lats[::-1], np.pi / 12. * times[::-1],
278
		fill_value="extrapolate",
279
	)
280
	# datetime
281
	dts_retr_interpf = interp1d(lats[::-1], dts[::-1], fill_value="extrapolate")
282
283
	# equator values
284
	lon0 = np.degrees(lons_intpf(0.)) % 360.
285
	lst0 = (lst_intpf(0.) * 12. / np.pi) % 24.
286
	mst0 = (mst_intpf(0.) * 12. / np.pi) % 24.
287
	time0 = (time_intpf(0.) * 12. / np.pi) % 24.
288
	dts_retr_interp0 = dts_retr_interpf(0.)
289
	logging.debug("utc day at equator: %s", dts_retr_interp0)
290
	logging.debug("mean LST at equator: %s, apparent LST at equator: %s", mst0, lst0)
291
292
	sdd.utchour = (time_intpf(sdd.lats) * 12. / np.pi) % 24.
293
	sdd.utcdays = dts_retr_interpf(sdd.lats)
294
295 View Code Duplication
	if sdd.lons is None:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
296
		# recalculate the longitudes
297
		# estimate the equatorial longitude from the
298
		# limb scan latitudes and longitudes
299
		lon0s_tp = lons - PHI_FAC * np.tan(np.radians(lats))
300
		clon0s_tp = np.cos(np.radians(lon0s_tp))
301
		slon0s_tp = np.sin(np.radians(lon0s_tp))
302
		lon0_tp = np.arctan2(np.sum(slon0s_tp[1:-1]), np.sum(clon0s_tp[1:-1]))
303
		lon0_tp = np.degrees((lon0_tp + 2. * np.pi) % (2. * np.pi))
304
		logging.info("lon0: %s", lon0)
305
		logging.info("lon0 tp: %s", lon0_tp)
306
		# interpolate to the retrieval latitudes
307
		tg_retr_lats = np.tan(np.radians(sdd.lats))
308
		calc_lons = (tg_retr_lats * PHI_FAC + lon0) % 360.
309
		calc_lons_tp = (tg_retr_lats * PHI_FAC + lon0_tp) % 360.
310
		sdd.lons = calc_lons_tp
311
		logging.debug("(calculated) retrieval lons: %s, %s",
312
				calc_lons, calc_lons_tp)
313
	else:
314
		# sdd.lons = sdd.lons % 360.
315
		logging.debug("(original) retrieval lons: %s", sdd.lons)
316
317
	sdd.mst = (sdd.utchour + sdd.lons / 15.) % 24.
318
	sdd.lst = sdd.mst + eotcorr / 60.
319
	mean_alt_km = sdd.alts.mean()
320
321
	dt_date_this = dt.timedelta(dts_retr_interp0.item()) + dtrefdate
322
	logging.info("date: %s", dt_date_this)
323
	# caclulate geomagnetic coordinates
324
	sdd.gmlats, sdd.gmlons = gmag_igrf(dt_date_this, sdd.lats, sdd.lons, alt=mean_alt_km)
325
	logging.debug("geomag. lats: %s, lons: %s", sdd.gmlats, sdd.gmlons)
326
	sdd.aacgmgmlats, sdd.aacgmgmlons = gmag_aacgm2005(sdd.lats, sdd.lons)
327
	logging.debug("aacgm geomag. lats: %s, lons: %s",
328
			sdd.aacgmgmlats, sdd.aacgmgmlons)
329
330
	# current day for MSIS input
331
	f107_data = _read_gm(F107_FILE)
332
	f107a_data = _read_gm(F107A_FILE)
333
	ap_data = _read_gm(AP_FILE)
334
	msis_dtdate = dt.timedelta(dts_retr_interp0.item()) + dtrefdate
335
	msis_dtdate1 = msis_dtdate - dt.timedelta(days=1)
336
	msis_date = msis_dtdate.strftime("%Y-%m-%d").encode()
337
	msis_date1 = msis_dtdate1.strftime("%Y-%m-%d").encode()
338
	msis_f107 = f107_data[msis_date1]
339
	msis_f107a = f107a_data[msis_date]
340
	msis_ap = ap_data[msis_date]
341
	logging.debug("MSIS date: %s, f10.7a: %s, f10.7: %s, ap: %s",
342
			msis_date, msis_f107a, msis_f107, msis_ap)
343
344
	# previous day for NOEM input
345
	f107_adj = _read_gm(F107_ADJ_FILE)
346
	kp_data = _read_gm(KP_FILE)
347
	noem_dtdate = dt.timedelta(dts_retr_interp0.item() - 1) + dtrefdate
348
	noem_date = noem_dtdate.strftime("%Y-%m-%d").encode()
349
	noem_f107 = f107_adj[noem_date]
350
	noem_kp = kp_data[noem_date]
351
	logging.debug("NOEM date: %s, f10.7: %s, kp: %s",
352
			noem_date, noem_f107, noem_kp)
353
354
	if sdd.noem_no is None:
355
		sdd.noem_no = np.zeros_like(sdd.densities)
356
	if sdd.temperature is None and msise is None:
357
		sdd.temperature = np.full_like(sdd.densities, np.nan)
358
	if sdd.sza is None:
359
		sdd.sza = np.zeros_like(sdd.lats)
360
	if sdd.akdiag is None:
361
		sdd.akdiag = np.zeros_like(sdd.densities)
362
		akm_filename = glob.glob(
363
				"{0}/000NO_orbit_{1:05d}_*_AKM*"
364
				.format(dens_path, orbit))[0]
365
		logging.debug("ak file: %s", akm_filename)
366
		ak = sa.read_akm(akm_filename, sdd.nalt, sdd.nlat)
367
		logging.debug("ak data: %s", ak)
368
		sdd.akdiag = ak.diagonal(axis1=1, axis2=3).diagonal(axis1=0, axis2=1)
369
370
	if msise is not None:
371
		if sdd.temperature is None or use_msis:
372
			_msis_d_t = msise(
373
				msis_dtdate,
374
				sdd.alts[None, :], sdd.lats[:, None], sdd.lons[:, None] % 360.,
375
				msis_f107a, msis_f107, msis_ap,
376
				lst=sdd.lst[:, None],
377
			)
378
			sdd.temperature = _msis_d_t[:, :, -1]
379
		if use_msis:
380
			sdd.dens_tot = np.sum(_msis_d_t[:, :, np.r_[:5, 6:9]], axis=2)
0 ignored issues
show
introduced by
The variable _msis_d_t does not seem to be defined in case sdd.temperature is None or use_msis on line 371 is False. Are you sure this can never be the case?
Loading history...
381
	for i, lat in enumerate(sdd.lats):
382
		if noem_cpp is not None:
383
			sdd.noem_no[i] = noem_cpp(noem_date.decode(), sdd.alts,
384
					[lat], [sdd.lons[i]], noem_f107, noem_kp)[:]
385
		else:
386
			sdd.noem_no[i][:] = np.nan
387
	sdd.sza[:] = solar_zenith_angle(
388
			mean_alt_km,
389
			sdd.lats, sdd.lons,
390
			(pd.to_timedelta(sdd.utcdays, unit="days") + dtrefdate).to_pydatetime(),
391
	)
392
	sdd.vmr = sdd.densities / sdd.dens_tot * 1.e9  # ppb
393
	return dts_retr_interp0, time0, lst0, lon0, sdd
394
395
396 View Code Duplication
def get_orbits_from_date(date, mlt=False, path=None, L2_version="v6.2"):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
397
	"""Find SCIAMACHY orbits with retrieved data at a date
398
399
	Parameters
400
	----------
401
	date: str
402
		The date in the format "%Y-%m-%d".
403
	mlt: bool, optional
404
		Look for MLT mode data instead of nominal mode data.
405
		Increases the heuristics to find all MLT orbits.
406
	path: str, optional
407
		The path to the level 2 data. If `None` tries to infer
408
		the data directory from the L2 version using
409
		'./*<L2_version>'. Default: None
410
411
	Returns
412
	-------
413
	orbits: list
414
		List of found orbits with retrieved data files
415
	"""
416
	logging.debug("pre-processing: %s", date)
417
	if path is None:
418
		density_base = os.curdir
419
		path = "{0}/*{1}".format(density_base, L2_version)
420
		logging.debug("path: %s", path)
421
422
	dfiles = glob.glob("{0}/000NO_orbit_*_{1}_Dichten.txt".format(
423
				path, date.replace("-", "")))
424
	orbits = sorted([int(os.path.basename(df).split('_')[2]) for df in dfiles])
425
	if mlt:
426
		orbits.append(orbits[-1] + 1)
427
	return orbits
428
429
430
def combine_orbit_data(orbits,
431
		ref_date="2000-01-01",
432
		L2_version="v6.2", file_version="2.2",
433
		dens_path=None, spec_base=None,
434
		use_xarray=False, save_nc=False):
435
	"""Combine post-processed SCIAMACHY retrieved orbit data
436
437
	Parameters
438
	----------
439
	orbits: list
440
		List of SCIAMACHY/Envisat orbit numbers to process.
441
	ref_date: str, optional
442
		Base date to calculate the relative days from,
443
		of the format "%Y-%m-%d". Default: 2000-01-01
444
	L2_version: str, optional
445
		SCIAMACHY level 2 data version to process
446
	file_version: str, optional
447
		Postprocessing format version of the output data
448
	dens_path: str, optional
449
		The path to the level 2 data. If `None` tries to infer
450
		the data directory from the L2 version looking for anything
451
		in the current directory that ends in <L2_version>: './*<L2_version>'.
452
		Default: None
453
	spec_base: str, optional
454
		The root path to the level 1c spectra. Uses the current
455
		dir if not set or set to `None` (default).
456
	use_xarray: bool, optional
457
		Uses xarray (if available) to combine the orbital data.
458
	save_nc: bool, optional
459
		Save the intermediate orbit data sets to netcdf files
460
		for debugging.
461
462
	Returns
463
	-------
464
	(sdday, sdday_ds): tuple
465
		`sdday` contains the combined data as a `scia_density_day` instance,
466
		`sdday_ds` contains the same data as a `xarray.Dataset`.
467
	"""
468
	if dens_path is None:
469
		# try some heuristics
470
		density_base = os.curdir
471
		dens_path = "{0}/*{1}".format(density_base, L2_version)
472
473
	sdday = sd.scia_density_day(ref_date=ref_date)
474
	sddayl = []
475
	sdday_ds = None
476
	for orbit in sorted(orbits):
477
		dateo, timeo, lsto, lono, sdens = process_orbit(orbit,
478
				ref_date=ref_date, dens_path=dens_path, spec_base=spec_base)
479
		logging.info(
480
			"orbit: %s, eq. date: %s, eq. hour: %s, eq. app. lst: %s, eq. lon: %s",
481
			orbit, dateo, timeo, lsto, lono
482
		)
483
		if sdens is not None:
484
			sdens.version = file_version
485
			sdens.data_version = L2_version
486
			sdday.append_data(dateo, orbit, timeo, sdens)
487
			if use_xarray:
488
				sd_xr = sdens.to_xarray(dateo, orbit)
489
				if sd_xr is not None:
490
					logging.debug("orbit %s dataset: %s", orbit, sd_xr)
491
					sddayl.append(sd_xr)
492
			if save_nc:
493
				sdens.write_to_netcdf(sdens.filename[:-3] + "nc")
494
	if use_xarray and sddayl:
495
		sdday_ds = xr.concat(sddayl, dim="time")
496
	return sdday, sdday_ds
497
498
499
VAR_ATTRS = {
500
	"2.1": {
501
		"MSIS_Dens": dict(
502
			units='cm^{-3}',
503
			long_name='total number density (NRLMSIS-00)',
504
		),
505
		"MSIS_Temp": dict(
506
			units='K',
507
			long_name='temperature',
508
			model="NRLMSIS-00",
509
		),
510
	},
511
	"2.2": {
512
	},
513
	"2.3": {
514
		"aacgm_gm_lats": dict(
515
			long_name='geomagnetic_latitude',
516
			model='AACGM2005 at 80 km',
517
			units='degrees_north',
518
		),
519
		"aacgm_gm_lons": dict(
520
			long_name='geomagnetic_longitude',
521
			model='AACGM2005 at 80 km',
522
			units='degrees_east',
523
		),
524
		"orbit": dict(
525
			axis='T', calendar='standard',
526
			long_name='SCIAMACHY/Envisat orbit number',
527
			standard_name="orbit",
528
			units='1',
529
		),
530
	},
531
}
532
VAR_RENAME = {
533
	"2.1": {
534
		# Rename to v2.1 variable names
535
		"MSIS_Dens": "TOT_DENS",
536
		"MSIS_Temp": "temperature",
537
	},
538
	"2.2": {
539
	},
540
	"2.3": {
541
	},
542
}
543
FLOAT_VARS = [
544
	"altitude", "latitude", "longitude",
545
	"app_LST", "mean_LST", "mean_SZA",
546
	"aacgm_gm_lats", "aacgm_gm_lons",
547
	"gm_lats", "gm_lons",
548
]
549
550
551
def sddata_xr_set_attrs(
552
	sdday_xr,
553
	file_version="2.2",
554
	ref_date="2000-01-01",
555
	rename=True,
556
	species="NO",
557
):
558
	"""Customize xarray Dataset variables and attributes
559
560
	Changes the variable names to match those exported from the
561
	`scia_density_day` class.
562
563
	Parameters
564
	----------
565
	sdday_xr: `xarray.Dataset` instance
566
		The combined dataset.
567
	file_version: string "major.minor", optional
568
		The netcdf file datase version, determines some variable
569
		names and attributes.
570
	ref_date: str, optional
571
		Base date to calculate the relative days from,
572
		of the format "%Y-%m-%d". Default: 2000-01-01
573
	rename: bool, optional
574
		Rename the dataset variables to match the
575
		`scia_density_day` exported ones.
576
		Default: True
577
	species: str, optional
578
		The name of the level 2 species, used to prefix the
579
		dataset variables to be named <species>_<variable>.
580
		Default: "NO".
581
	"""
582
	if rename:
583
		sdday_xr = sdday_xr.rename({
584
			# 2d vars
585
			"akm_diagonal": "{0}_AKDIAG".format(species),
586
			"apriori": "{0}_APRIORI".format(species),
587
			"density": "{0}_DENS".format(species),
588
			"density_air": "MSIS_Dens",
589
			"error_meas": "{0}_ERR".format(species),
590
			"error_tot": "{0}_ETOT".format(species),
591
			"temperature": "MSIS_Temp",
592
			"NOEM_density": "{0}_NOEM".format(species),
593
			"VMR": "{0}_VMR".format(species),
594
			# 1d vars and dimensions
595
			"app_lst": "app_LST",
596
			"mean_lst": "mean_LST",
597
			"mean_sza": "mean_SZA",
598
			"utc_hour": "UTC",
599
		})
600
	# relative standard deviation
601
	sdday_xr["{0}_RSTD".format(species)] = 100.0 * np.abs(
602
			sdday_xr["{0}_ERR".format(species)] / sdday_xr["{0}_DENS".format(species)])
603
	# fix coordinate attributes
604
	sdday_xr["time"].attrs = dict(axis='T', standard_name='time',
605
		calendar='standard', long_name='equatorial crossing time',
606
		units="days since {0}".format(
607
			pd.to_datetime(ref_date, utc=True).isoformat(sep=" ")))
608
	sdday_xr["altitude"].attrs = dict(axis='Z', positive='up',
609
		long_name='altitude', standard_name='altitude', units='km')
610
	sdday_xr["latitude"].attrs = dict(axis='Y', long_name='latitude',
611
		standard_name='latitude', units='degrees_north')
612
	# Default variable attributes
613
	sdday_xr["{0}_DENS".format(species)].attrs = {
614
			"units": "cm^{-3}",
615
			"long_name": "{0} number density".format(species)}
616
	sdday_xr["{0}_ERR".format(species)].attrs = {
617
			"units": "cm^{-3}",
618
			"long_name": "{0} density measurement error".format(species)}
619
	sdday_xr["{0}_ETOT".format(species)].attrs = {
620
			"units": "cm^{-3}",
621
			"long_name": "{0} density total error".format(species)}
622
	sdday_xr["{0}_RSTD".format(species)].attrs = dict(
623
			units='%',
624
			long_name='{0} relative standard deviation'.format(species))
625
	sdday_xr["{0}_AKDIAG".format(species)].attrs = dict(
626
			units='1',
627
			long_name='{0} averaging kernel diagonal element'.format(species))
628
	sdday_xr["{0}_APRIORI".format(species)].attrs = dict(
629
			units='cm^{-3}', long_name='{0} apriori density'.format(species))
630
	sdday_xr["{0}_NOEM".format(species)].attrs = dict(
631
			units='cm^{-3}', long_name='NOEM {0} number density'.format(species))
632
	sdday_xr["{0}_VMR".format(species)].attrs = dict(
633
			units='ppb', long_name='{0} volume mixing ratio'.format(species))
634
	sdday_xr["MSIS_Dens"].attrs = dict(units='cm^{-3}',
635
			long_name='MSIS total number density',
636
			model="NRLMSIS-00")
637
	sdday_xr["MSIS_Temp"].attrs = dict(units='K',
638
			long_name='MSIS temperature',
639
			model="NRLMSIS-00")
640
	sdday_xr["longitude"].attrs = dict(long_name='longitude',
641
			standard_name='longitude', units='degrees_east')
642
	sdday_xr["app_LST"].attrs = dict(units='hours',
643
			long_name='apparent local solar time')
644
	sdday_xr["mean_LST"].attrs = dict(units='hours',
645
			long_name='mean local solar time')
646
	sdday_xr["mean_SZA"].attrs = dict(units='degrees',
647
			long_name='solar zenith angle at mean altitude')
648
	sdday_xr["UTC"].attrs = dict(units='hours',
649
			long_name='measurement utc time')
650
	sdday_xr["utc_days"].attrs = dict(
651
			units='days since {0}'.format(
652
				pd.to_datetime(ref_date, utc=True).isoformat(sep=" ")),
653
			long_name='measurement utc day')
654
	sdday_xr["gm_lats"].attrs = dict(long_name='geomagnetic_latitude',
655
			model='IGRF', units='degrees_north')
656
	sdday_xr["gm_lons"].attrs = dict(long_name='geomagnetic_longitude',
657
			model='IGRF', units='degrees_east')
658
	sdday_xr["aacgm_gm_lats"].attrs = dict(long_name='geomagnetic_latitude',
659
			# model='AACGM2005 80 km',  # v2.3
660
			model='AACGM',  # v2.1, v2.2
661
			units='degrees_north')
662
	sdday_xr["aacgm_gm_lons"].attrs = dict(long_name='geomagnetic_longitude',
663
			# model='AACGM2005 80 km',  # v2.3
664
			model='AACGM',  # v2.1, v2.2
665
			units='degrees_east')
666
	sdday_xr["orbit"].attrs = dict(
667
			axis='T', calendar='standard',
668
			# long_name='SCIAMACHY/Envisat orbit number',  # v2.3
669
			long_name='orbit',  # v2.1, v2.2
670
			standard_name="orbit",
671
			# units='1',  # v2.3
672
			units='orbit number',  # v2.1, v2.2
673
	)
674
	# Overwrite version-specific variable attributes
675
	for _v, _a in VAR_ATTRS[file_version].items():
676
		sdday_xr[_v].attrs = _a
677
	if rename:
678
		# version specific renaming
679
		sdday_xr = sdday_xr.rename(VAR_RENAME[file_version])
680
	if int(file_version.split(".")[0]) < 3:
681
		# invert latitudes for backwards-compatitbility
682
		sdday_xr = sdday_xr.sortby("latitude", ascending=False)
683
	else:
684
		sdday_xr = sdday_xr.sortby("latitude", ascending=True)
685
686
	sdday_xr.attrs["software"] = "sciapy {0}".format(__version__)
687
688
	dateo = pd.to_datetime(
689
			xr.conventions.decode_cf(sdday_xr[["time"]]).time.data[0],
690
			utc=True,
691
	).strftime("%Y-%m-%d")
692
	logging.debug("date %s dataset: %s", dateo, sdday_xr)
693
	return sdday_xr
694
695
696
def main():
697
	logging.basicConfig(level=logging.WARNING,
698
			format="[%(levelname)-8s] (%(asctime)s) "
699
			"%(filename)s:%(lineno)d %(message)s",
700
			datefmt="%Y-%m-%d %H:%M:%S %z")
701
702
	parser = ap.ArgumentParser()
703
	parser.add_argument("file", default="SCIA_NO.nc",
704
			help="the filename of the output netcdf file")
705
	parser.add_argument("-M", "--month", metavar="YEAR-MM",
706
			help="infer start and end dates for month")
707
	parser.add_argument("-D", "--date_range", metavar="START_DATE:END_DATE",
708
			help="colon-separated start and end dates")
709
	parser.add_argument("-d", "--dates", help="comma-separated list of dates")
710
	parser.add_argument("-B", "--base_date",
711
			metavar="YEAR-MM-DD", default="2000-01-01",
712
			help="Reference date to base the time values (days) on "
713
			"(default: %(default)s).")
714
	parser.add_argument("-f", "--orbit_file",
715
			help="the file containing the input orbits")
716
	parser.add_argument("-r", "--retrieval_version", default="v6.2",
717
			help="SCIAMACHY level 2 data version to process")
718
	parser.add_argument("-R", "--file_version", default="2.2",
719
			help="Postprocessing format version of the output file")
720
	parser.add_argument("-A", "--author", default="unknown",
721
			help="Author of the post-processed data set "
722
			"(default: %(default)s)")
723
	parser.add_argument("-p", "--path", default=None,
724
			help="path containing the L2 data")
725
	parser.add_argument("-s", "--spectra", default=None, metavar="PATH",
726
			help="path containing the L1c spectra")
727
	parser.add_argument("-m", "--mlt", action="store_true", default=False,
728
			help="indicate nominal (False, default) or MLT data (True)")
729
	parser.add_argument("-X", "--xarray", action="store_true", default=False,
730
			help="use xarray to prepare the dataset"
731
			" (experimental, default %(default)s)")
732
	loglevels = parser.add_mutually_exclusive_group()
733
	loglevels.add_argument("-l", "--loglevel", default="WARNING",
734
			choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
735
			help="change the loglevel (default: 'WARNING')")
736
	loglevels.add_argument("-q", "--quiet", action="store_true", default=False,
737
			help="less output, same as --loglevel=ERROR (default: False)")
738
	loglevels.add_argument("-v", "--verbose", action="store_true", default=False,
739
			help="verbose output, same as --loglevel=INFO (default: False)")
740
	args = parser.parse_args()
741
	if args.quiet:
742
		logging.getLogger().setLevel(logging.ERROR)
743
	elif args.verbose:
744
		logging.getLogger().setLevel(logging.INFO)
745
	else:
746
		logging.getLogger().setLevel(args.loglevel)
747
748
	logging.info("processing L2 version: %s", args.retrieval_version)
749
	logging.info("writing data file version: %s", args.file_version)
750
751
	pddrange = []
752
	if args.month is not None:
753
		d0 = pd.to_datetime(args.month + "-01", utc=True)
754
		pddrange.extend(pd.date_range(d0, d0 + pd.tseries.offsets.MonthEnd()))
755
	if args.date_range is not None:
756
		pddrange.extend(pd.date_range(*args.date_range.split(':')))
757
	if args.dates is not None:
758
		pddrange.extend(pd.to_datetime(args.dates.split(','), utc=True))
759
	logging.debug("pddrange: %s", pddrange)
760
761
	olist = []
762
	for date in pddrange:
763
		try:
764
			olist += get_orbits_from_date(date.strftime("%Y-%m-%d"),
765
				mlt=args.mlt, path=args.path, L2_version=args.retrieval_version)
766
		except:  # handle NaT
767
			pass
768
	if args.orbit_file is not None:
769
		olist += np.genfromtxt(args.orbit_file, dtype=np.int32).tolist()
770
	logging.debug("olist: %s", olist)
771
772
	if not olist:
773
		logging.warn("No orbits to process.")
774
		return
775
776
	sdlist, sdxr_ds = combine_orbit_data(olist,
777
			ref_date=args.base_date,
778
			L2_version=args.retrieval_version, file_version=args.file_version,
779
			dens_path=args.path, spec_base=args.spectra, use_xarray=args.xarray,
780
			save_nc=False)
781
	sdlist.author = args.author
782
783
	if args.xarray and sdxr_ds is not None:
784
		sdxr_ds.attrs["author"] = args.author
785
		sd_xr = sddata_xr_set_attrs(
786
			sdxr_ds, ref_date=args.base_date,
787
			rename=True, file_version=args.file_version,
788
		)
789
		sd_xr2 = sdlist.to_xarray()
790
		# Overwrite version-specific variable attributes
791
		for _v, _a in VAR_ATTRS[args.file_version].items():
792
			sd_xr2[_v].attrs = _a
793
		# version specific renaming
794
		sd_xr2 = sd_xr2.rename(VAR_RENAME[args.file_version])
795
		logging.debug(sd_xr)
796
		logging.debug(sd_xr2)
797
		logging.debug("equal datasets: %s", sd_xr.equals(sd_xr2))
798
		xr.testing.assert_allclose(sd_xr, sd_xr2)
799
		if sd_xr2 is not None:
800
			logging.debug("xarray dataset: %s", sd_xr2)
801
			sd_xr2.to_netcdf(args.file, unlimited_dims=["time"])
802
	else:
803
		if sdlist.no_dens is not None:
804
			sdlist.write_to_netcdf(args.file)
805
		else:
806
			logging.warn("Processed data is empty.")
807
808
809
if __name__ == "__main__":
810
	logging.warn(
811
		"%s: This script is deprecated, use 'python -m sciapy.level2.post_process' instead.",
812
		DeprecationWarning.__name__,
813
	)
814
	main()
815