1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# vim:fileencoding=utf-8 |
3
|
|
|
# |
4
|
|
|
# Copyright (c) 2014-2018 Stefan Bender |
5
|
|
|
# |
6
|
|
|
# This module is part of sciapy. |
7
|
|
|
# sciapy is free software: you can redistribute it or modify |
8
|
|
|
# it under the terms of the GNU General Public License as published |
9
|
|
|
# by the Free Software Foundation, version 2. |
10
|
|
|
# See accompanying LICENSE file or http://www.gnu.org/licenses/gpl-2.0.html. |
11
|
1 |
|
"""SCIAMACHY level 1c limb spectra module |
12
|
|
|
|
13
|
|
|
This module contains the class for SCIAMACHY level 1c limb spectra. |
14
|
|
|
It include some simple conversion routines: from and to ascii, |
15
|
|
|
from and to binary, from and to netcdf. |
16
|
|
|
|
17
|
|
|
A simple import from HDF5 files produced by the SRON nadc_tools |
18
|
|
|
(https://github.com/rmvanhees/nadc_tools) is also supported. |
19
|
|
|
""" |
20
|
1 |
|
from __future__ import absolute_import, division, print_function |
21
|
|
|
|
22
|
1 |
|
import numpy as np |
23
|
|
|
|
24
|
1 |
|
__all__ = ["scia_limb_point", "scia_limb_scan"] |
25
|
|
|
|
26
|
1 |
|
def _equation_of_time(doy): |
27
|
|
|
"""Equation of time correction for day of year (doy) |
28
|
|
|
|
29
|
|
|
See: |
30
|
|
|
https://en.wikipedia.org/wiki/Equation_of_time |
31
|
|
|
|
32
|
|
|
Parameters |
33
|
|
|
---------- |
34
|
|
|
doy: int |
35
|
|
|
Day of year, Jan 1 = 1 |
36
|
|
|
|
37
|
|
|
Returns |
38
|
|
|
------- |
39
|
|
|
eot: float |
40
|
|
|
Equation of time correction in minutes |
41
|
|
|
""" |
42
|
1 |
|
D = doy - 1 # jan 1 = day zero |
43
|
1 |
|
W = 360.0 / 365.242 |
44
|
1 |
|
A = W * (D + 10) |
45
|
1 |
|
B = A + 1.914 * np.sin(np.radians(W * (D - 2))) |
46
|
1 |
|
C = (np.radians(A) - |
47
|
|
|
np.arctan2(np.tan(np.radians(B)), |
48
|
|
|
np.cos(np.radians(23.44)))) / np.pi |
49
|
1 |
|
return 720.0 * (C - round(C)) |
50
|
|
|
|
51
|
1 |
|
class scia_limb_point(object): |
52
|
|
|
"""SCIAMACHY limb tangent point data |
53
|
|
|
|
54
|
|
|
Contains the data from a single tangent point.""" |
55
|
1 |
|
def __init__(self, ls, i): |
56
|
|
|
self.date = [] |
57
|
|
|
self.npix = 0 |
58
|
|
|
self.orbit = 0 |
59
|
|
|
self.sub_sat_lat = None |
60
|
|
|
self.sub_sat_lon = None |
61
|
|
|
self.tp_lat = None |
62
|
|
|
self.tp_lon = None |
63
|
|
|
self.tp_alt = None |
64
|
|
|
self.tp_sza = None |
65
|
|
|
self.tp_saa = None |
66
|
|
|
self.tp_los_zenith = None |
67
|
|
|
self.toa_sza = None |
68
|
|
|
self.toa_saa = None |
69
|
|
|
self.toa_los_zenith = None |
70
|
|
|
self.sat_sza = None |
71
|
|
|
self.sat_saa = None |
72
|
|
|
self.sat_los_zenith = None |
73
|
|
|
self.sat_alt = None |
74
|
|
|
self.earthradius = None |
75
|
|
|
|
76
|
|
|
self.wls = [] |
77
|
|
|
self.rads = [] |
78
|
|
|
self.errs = [] |
79
|
|
|
self.limb_data = None |
80
|
|
|
self.from_limb_scan(ls, i) |
81
|
|
|
|
82
|
1 |
|
def from_limb_scan(self, ls, i): |
83
|
|
|
"""Import the spectra from a single tangent point of the limb scan |
84
|
|
|
|
85
|
|
|
Parameters |
86
|
|
|
---------- |
87
|
|
|
ls : :class:`~sciapy.level1c.scia_limb_scan` |
88
|
|
|
The SCIAMACHY limb scan from which to extract the spectra. |
89
|
|
|
i : int |
90
|
|
|
The number of the tangent point in the limb scan |
91
|
|
|
""" |
92
|
|
|
self.date = ls.date |
93
|
|
|
self.npix = ls.npix |
94
|
|
|
self.orbit = ls.orbit |
95
|
|
|
if np.any(ls.limb_data.sub_sat_lat): |
96
|
|
|
self.sub_sat_lat = ls.limb_data.sub_sat_lat[i] |
97
|
|
|
self.sub_sat_lon = ls.limb_data.sub_sat_lon[i] |
98
|
|
|
self.tp_lat = ls.limb_data.tp_lat[i] |
99
|
|
|
self.tp_lon = ls.limb_data.tp_lon[i] |
100
|
|
|
self.tp_alt = ls.limb_data.tp_alt[i] |
101
|
|
|
self.tp_sza = ls.limb_data.tp_sza[i] |
102
|
|
|
self.tp_saa = ls.limb_data.tp_saa[i] |
103
|
|
|
self.tp_los_zenith = ls.limb_data.tp_los[i] |
104
|
|
|
self.toa_sza = ls.limb_data.toa_sza[i] |
105
|
|
|
self.toa_saa = ls.limb_data.toa_saa[i] |
106
|
|
|
self.toa_los_zenith = ls.limb_data.toa_los[i] |
107
|
|
|
self.sat_sza = ls.limb_data.sat_sza[i] |
108
|
|
|
self.sat_saa = ls.limb_data.sat_saa[i] |
109
|
|
|
self.sat_los_zenith = ls.limb_data.sat_los[i] |
110
|
|
|
self.sat_alt = ls.limb_data.sat_alt[i] |
111
|
|
|
self.earthradius = ls.limb_data.earth_rad[i] |
112
|
|
|
|
113
|
|
|
self.wls = ls.wls |
114
|
|
|
self.rads = ls.limb_data.rad[i] |
115
|
|
|
self.errs = ls.limb_data.err[i] |
116
|
|
|
self.limb_data = ls.limb_data[i] |
117
|
|
|
|
118
|
|
|
|
119
|
1 |
|
class scia_limb_scan(object): |
120
|
|
|
"""SCIAMACHY limb scan data |
121
|
|
|
|
122
|
|
|
Contains the data from all or some selected tangent points. |
123
|
|
|
The format is inspired by the SCIAMACHY ascii data format. |
124
|
|
|
|
125
|
|
|
Attributes |
126
|
|
|
---------- |
127
|
|
|
textheader_length : int |
128
|
|
|
The number of lines of the text header. |
129
|
|
|
textheader : str |
130
|
|
|
The header containing the limb scan meta data. |
131
|
|
|
metadata : dict |
132
|
|
|
Metadata of the limb scan as parsed by |
133
|
|
|
:func:`parse_textheader`. Contains: |
134
|
|
|
|
135
|
|
|
datatype_txt: str |
136
|
|
|
The name of the data type. |
137
|
|
|
l1b_product: str |
138
|
|
|
The level 1b product which was calibrated. |
139
|
|
|
orbit: int |
140
|
|
|
The Envisat/SCIAMACHY orbit number. |
141
|
|
|
state_id: int |
142
|
|
|
The SCIAMACHY state_id, denotes the measurement type |
143
|
|
|
that was carried out, i.e. nominal limb, MLT limb, |
144
|
|
|
nadir, sun or moon occultation etc. |
145
|
|
|
software_version: str |
146
|
|
|
The software used for calibration. |
147
|
|
|
keyfile_version: str |
148
|
|
|
The keyfile version used in the calibration process. |
149
|
|
|
mfactor_version: str |
150
|
|
|
The M-factor version used in the calibration process. |
151
|
|
|
init_version: str |
152
|
|
|
The init version used in the calibration process. |
153
|
|
|
decont_flags: str |
154
|
|
|
The decont flags used in the calibration process. |
155
|
|
|
calibration: str |
156
|
|
|
The calibrations that were applied to the level 1b data |
157
|
|
|
to produce the level 1c data. |
158
|
|
|
date: str |
159
|
|
|
The measurement data of the limb scan as "%Y%m%d" |
160
|
|
|
nr_profile: int |
161
|
|
|
The number of profiles in the scan. |
162
|
|
|
act_profile: int |
163
|
|
|
The number of the current profile. |
164
|
|
|
nalt : int |
165
|
|
|
The number of tangent points. |
166
|
|
|
npix : int |
167
|
|
|
The number of spectral points. |
168
|
|
|
orbit_state : tuple or list of ints |
169
|
|
|
Orbit state data containing |
170
|
|
|
(orbit number, state in orbit, state id, |
171
|
|
|
number of profiles per state (usually one), |
172
|
|
|
the actual profile number). |
173
|
|
|
date : tuple or list of ints |
174
|
|
|
The limb scan's date (year, month, day, hour, minute, second). |
175
|
|
|
cent_lat_lon : tuple or list of float |
176
|
|
|
The centre latitude and longitude of the scan followed by the |
177
|
|
|
four corner latitude and longitude: |
178
|
|
|
(lat_centre, lon_centre, lat_corner0, lon_corner0, ..., |
179
|
|
|
lat_corner3, lon_corner3). |
180
|
|
|
orbit_phase : float |
181
|
|
|
The orbital phase of the limb scan. |
182
|
|
|
wls: (N,) array_like |
183
|
|
|
The spectral wavelengths. |
184
|
|
|
limb_data : numpy.recarray |
185
|
|
|
The limb data containing the following records: |
186
|
|
|
|
187
|
|
|
sub_sat_lat: (M,) array_like |
188
|
|
|
The latitudes of the satellite ground points (M = nalt). |
189
|
|
|
sub_sat_lon: (M,) array_like |
190
|
|
|
The longitudes of the satellite ground points (M = nalt). |
191
|
|
|
tp_lat: (M,) array_like |
192
|
|
|
The latitudes of the tangent points (M = nalt). |
193
|
|
|
tp_lon: (M,) array_like |
194
|
|
|
The longitudes of the tangent points (M = nalt). |
195
|
|
|
tp_alt: (M,) array_like |
196
|
|
|
The tangent altitudes (M = nalt). |
197
|
|
|
tp_sza: (M,) array_like |
198
|
|
|
The solar zenith angles at the tangent points (M = nalt). |
199
|
|
|
tp_saa: (M,) array_like |
200
|
|
|
The solar azimuth angles at the tangent points (M = nalt). |
201
|
|
|
tp_los: (M,) array_like |
202
|
|
|
The line-of-sight zenith angles at the tangent points (M = nalt). |
203
|
|
|
toa_sza: (M,) array_like |
204
|
|
|
The solar zenith angles at the top-of-atmosphere points (M = nalt). |
205
|
|
|
toa_saa: (M,) array_like |
206
|
|
|
The solar azimuth angles at the top-of-atmosphere points (M = nalt). |
207
|
|
|
toa_los: (M,) array_like |
208
|
|
|
The line-of-sight zenith angles at the top-of-atmosphere points (M = nalt). |
209
|
|
|
sat_sza: (M,) array_like |
210
|
|
|
The solar zenith angles at the satellite points (M = nalt). |
211
|
|
|
sat_saa: (M,) array_like |
212
|
|
|
The solar azimuth angles at the satellite points (M = nalt). |
213
|
|
|
sat_los: (M,) array_like |
214
|
|
|
The line-of-sight zenith angles at the satellite points (M = nalt). |
215
|
|
|
sat_alt: (M,) array_like |
216
|
|
|
The satellite altitudes (M = nalt). |
217
|
|
|
earth_rad: (M,) array_like |
218
|
|
|
The earth radii at the tangent ground points (M = nalt). |
219
|
|
|
rad: (M, N) array_like |
220
|
|
|
The radiances at the tangent points, M = nalt, N = len(wls). |
221
|
|
|
err: (M, N) array_like |
222
|
|
|
The relative radiance uncertainties at the tangent points, |
223
|
|
|
M = nalt, N = len(wls). |
224
|
|
|
""" |
225
|
1 |
|
from .scia_limb_nc import read_from_netcdf, write_to_netcdf |
226
|
1 |
|
from .scia_limb_txt import read_from_textfile, write_to_textfile |
227
|
1 |
|
from .scia_limb_mpl import read_from_mpl_binary, write_to_mpl_binary |
228
|
1 |
|
from .scia_limb_hdf5 import (read_hdf5_limb_state_common_data, |
229
|
|
|
read_hdf5_limb_state_spectral_data, |
230
|
|
|
read_from_hdf5) |
231
|
|
|
|
232
|
1 |
|
def __init__(self): |
233
|
1 |
|
self._limb_data_dtype = None |
234
|
1 |
|
self.textheader_length = 0 |
235
|
1 |
|
self.textheader = "" |
236
|
1 |
|
self.metadata = {} |
237
|
1 |
|
self.nalt = 0 |
238
|
1 |
|
self.npix = 0 |
239
|
1 |
|
self.orbit_state = [] |
240
|
1 |
|
(self.orbit, self.state_in_orbit, self.state_id, |
241
|
|
|
self.profiles_per_state, self.profile_in_state) = (0, 0, 0, 0, 0) |
242
|
1 |
|
self.date = [] |
243
|
1 |
|
self.cent_lat_lon = [] |
244
|
1 |
|
self.orbit_phase = 0. |
245
|
|
|
|
246
|
1 |
|
self.limb_data = None |
247
|
|
|
|
248
|
1 |
|
self.wls = [] |
249
|
|
|
|
250
|
1 |
|
def parse_textheader(self): |
251
|
|
|
"""Parses the ASCII header metadata |
252
|
|
|
|
253
|
|
|
The ASCII header text contains metadata about the current limb scan. |
254
|
|
|
This function reads this metadata into the :attr:`metadata` dictionary. |
255
|
|
|
""" |
256
|
1 |
|
from parse import parse |
257
|
1 |
|
split_header = self.textheader.split('\n') |
258
|
1 |
|
line = 0 |
259
|
1 |
|
res = parse("#Data type : {txt}", split_header[line]) |
260
|
1 |
|
self.metadata["datatype_txt"] = res["txt"] |
261
|
1 |
|
line += 1 |
262
|
1 |
|
res = parse("#L1b product : {product}", split_header[line]) |
263
|
1 |
|
self.metadata["l1b_product"] = res["product"] |
264
|
1 |
|
line += 1 |
265
|
1 |
|
res = parse("#Orbit nr.,State ID : {orbit:05d} {state_id:2d}", split_header[line]) |
266
|
1 |
|
self.metadata["orbit"] = res["orbit"] |
267
|
1 |
|
self.metadata["state_id"] = res["state_id"] |
268
|
1 |
|
line += 1 |
269
|
1 |
|
res = parse("#Ver. Proc/Key/M/I/D: {soft}{:s}{key} {mf} {init} {decont}", |
270
|
|
|
split_header[line]) |
271
|
1 |
|
self.metadata["software_version"] = res["soft"] |
272
|
1 |
|
self.metadata["keyfile_version"] = res["key"] |
273
|
1 |
|
self.metadata["mfactor_version"] = res["mf"] |
274
|
1 |
|
self.metadata["init_version"] = res["init"] |
275
|
1 |
|
self.metadata["decont_flags"] = res["decont"] |
276
|
1 |
|
line += 1 |
277
|
1 |
|
res = parse("#Calibr. appl. (0-8): {cal}", split_header[line]) |
278
|
1 |
|
self.metadata["calibration"] = res["cal"] |
279
|
1 |
|
line += 1 |
280
|
1 |
|
res = parse("#State Starttime : {date}", split_header[line]) |
281
|
1 |
|
self.metadata["date"] = res["date"] |
282
|
1 |
|
line += 1 |
283
|
1 |
|
res = parse("#Nr Profiles / act. : {np:3d} {ap:3d}", split_header[line]) |
284
|
1 |
|
self.metadata["nr_profile"] = res["np"] |
285
|
1 |
|
self.metadata["act_profile"] = res["ap"] |
286
|
|
|
|
287
|
1 |
|
def assemble_textheader(self): |
288
|
|
|
"""Combines the metadata to ASCII header |
289
|
|
|
|
290
|
|
|
Tranfers the :attr:`metadata` dictionary back to ASCII form |
291
|
|
|
for writing to disk. |
292
|
|
|
""" |
293
|
|
|
# Prepare the header |
294
|
|
|
meta = self.metadata |
295
|
|
|
if not meta: |
296
|
|
|
return |
297
|
|
|
n_header = 30 |
298
|
|
|
line = n_header + 2 |
299
|
|
|
header = ("#Data type : {0[datatype_txt]}\n".format(meta)) |
300
|
|
|
header += ("#L1b product : {0[l1b_product]}\n".format(meta)) |
301
|
|
|
header += ("#Orbit nr.,State ID : {0:05d} {1:2d}\n".format(meta["orbit"], meta["state_id"])) |
302
|
|
|
header += ("#Ver. Proc/Key/M/I/D: {0[software_version]:14s} " |
303
|
|
|
"{0[keyfile_version]} {0[mfactor_version]} " |
304
|
|
|
"{0[init_version]} {0[decont_flags]}\n" |
305
|
|
|
.format(meta)) |
306
|
|
|
header += ("#Calibr. appl. (0-8): {0[calibration]}\n".format(meta)) |
307
|
|
|
header += ("#State Starttime : {0[date]}\n".format(meta)) |
308
|
|
|
header += ("#Nr Profiles / act. : {0[nr_profile]:3d} {0[act_profile]:3d}\n".format(meta)) |
309
|
|
|
header += ("# Angles TOA\n") |
310
|
|
|
header += ("#L.{0:2d} : Number_of_altitudes Number_of_pixels\n".format(line)) |
311
|
|
|
line += 1 |
312
|
|
|
header += ("#L.{0:2d} : Orbit State_in_orbit/file State-ID Profiles_per_state Profile_in_State\n".format(line)) |
313
|
|
|
line += 1 |
314
|
|
|
header += ("#L.{0:2d} : Date Time : yyyy mm dd hh mm ss\n".format(line)) |
315
|
|
|
line += 1 |
316
|
|
|
|
317
|
|
|
header += ("#L.{0:2d} : Sub satellite point lat\n".format(line)) |
318
|
|
|
line += 1 |
319
|
|
|
header += ("#L.{0:2d} : Sub satellite point lon\n".format(line)) |
320
|
|
|
line += 1 |
321
|
|
|
header += ("#L.{0:2d} : orbit phase [0..1]\n".format(line)) |
322
|
|
|
line += 1 |
323
|
|
|
|
324
|
|
|
header += ("#L.{0:2d} : Center(lat/lon) 4*Corners(lat/lon)\n".format(line)) |
325
|
|
|
line += 1 |
326
|
|
|
header += ("#L.{0:2d} : Tangent ground point lat\n".format(line)) |
327
|
|
|
line += 1 |
328
|
|
|
header += ("#L.{0:2d} : Tangent ground point lon\n".format(line)) |
329
|
|
|
line += 1 |
330
|
|
|
header += ("#L.{0:2d} : Tangent height\n".format(line)) |
331
|
|
|
line += 1 |
332
|
|
|
|
333
|
|
|
header += ("#L.{0:2d} : tangent pnt: Solar Zenith angle\n".format(line)) |
334
|
|
|
line += 1 |
335
|
|
|
header += ("#L.{0:2d} : tangent pnt: rel. Solar Azimuth angle\n".format(line)) |
336
|
|
|
line += 1 |
337
|
|
|
header += ("#L.{0:2d} : tangent pnt: LOS zenith\n".format(line)) |
338
|
|
|
line += 1 |
339
|
|
|
header += ("#L.{0:2d} : TOA: Solar Zenith angle\n".format(line)) |
340
|
|
|
line += 1 |
341
|
|
|
header += ("#L.{0:2d} : TOA: rel Solar Azimuth angle\n".format(line)) |
342
|
|
|
line += 1 |
343
|
|
|
header += ("#L.{0:2d} : TOA: LOS zenith\n".format(line)) |
344
|
|
|
line += 1 |
345
|
|
|
header += ("#L.{0:2d} : Sat: Solar Zenith angle\n".format(line)) |
346
|
|
|
line += 1 |
347
|
|
|
header += ("#L.{0:2d} : Sat: rel Solar Azimuth angle\n".format(line)) |
348
|
|
|
line += 1 |
349
|
|
|
header += ("#L.{0:2d} : Sat: LOS zenith\n".format(line)) |
350
|
|
|
line += 1 |
351
|
|
|
|
352
|
|
|
header += ("#L.{0:2d} : Sat. height\n".format(line)) |
353
|
|
|
line += 1 |
354
|
|
|
header += ("#L.{0:2d} : Earth radius\n".format(line)) |
355
|
|
|
line += 1 |
356
|
|
|
header += ("#L.{0:2d} : Npix lines : wavelength n_altitude x radiance".format(line)) |
357
|
|
|
self.textheader_length = n_header |
358
|
|
|
self.textheader = header |
359
|
|
|
|
360
|
1 |
|
def read_from_file(self, filename): |
361
|
|
|
"""SCIAMACHY level 1c limb scan general file import |
362
|
|
|
|
363
|
|
|
Tries `netcdf` first, the custom binary format second if |
364
|
|
|
netcdf fails, and finally ASCII import if that also fails. |
365
|
|
|
""" |
366
|
1 |
|
try: |
367
|
|
|
# try netcdf first |
368
|
1 |
|
self.read_from_netcdf(filename) |
369
|
1 |
|
except: |
370
|
1 |
|
try: |
371
|
|
|
# fall back to mpl binary |
372
|
1 |
|
self.read_from_mpl_binary(filename) |
373
|
|
|
except: |
374
|
|
|
# fall back to text file as a last resort |
375
|
|
|
self.read_from_textfile(filename) |
376
|
|
|
|
377
|
1 |
|
def local_solar_time(limb_scan, debug=True): |
378
|
|
|
"""Local solar time at limb scan footprint centre |
379
|
|
|
|
380
|
|
|
Returns |
381
|
|
|
------- |
382
|
|
|
(mean_lst, apparent_lst, eot_correction): tuple |
383
|
|
|
* mean_lst - mean local solar time |
384
|
|
|
* apparent_lst - apparent local solar time, equation of time corrected |
385
|
|
|
* eot_correction - equation of time correction in minutes |
386
|
|
|
""" |
387
|
1 |
|
import datetime as dt |
388
|
1 |
|
import logging |
389
|
1 |
|
dtime = dt.datetime(*limb_scan.date) |
390
|
1 |
|
doy = int(dtime.strftime("%j")) |
391
|
1 |
|
eot_correction = _equation_of_time(doy) |
392
|
1 |
|
hours, mins, secs = limb_scan.date[3:] |
393
|
1 |
|
clat, clon = limb_scan.cent_lat_lon[:2] |
394
|
1 |
|
if clon > 180.0: |
395
|
1 |
|
clon = clon - 360.0 |
396
|
1 |
|
mean_lst = hours + mins / 60. + secs / 3600. + clon / 15. |
397
|
1 |
|
apparent_lst = mean_lst + eot_correction / 60.0 |
398
|
|
|
|
399
|
1 |
|
if debug: |
400
|
|
|
logging.debug("%d %d %02d", |
401
|
|
|
limb_scan.orbit, limb_scan.state_in_orbit, doy) |
402
|
|
|
logging.debug("%s", limb_scan.orbit_state) |
403
|
|
|
logging.debug("%02d %02d %02d", hours, mins, secs) |
404
|
|
|
logging.debug("%.3f %.3f %.6f %.6f %.6f", clat, clon, |
405
|
|
|
mean_lst % 24, apparent_lst % 24, eot_correction) |
406
|
|
|
return mean_lst % 24, apparent_lst % 24, eot_correction |
407
|
|
|
|