OB.DAAC Logo
NASA Logo
Ocean Color Science Software

ocssw V2022
l1bextract_safe_nc.py
Go to the documentation of this file.
1 #! /usr/bin/env python3
2 
3 # Extractor for OLCI Sentinel 3 and MERIS SAFE formatted L1B files
4 
5 import argparse
6 from datetime import datetime, timedelta
7 import netCDF4
8 import pathlib
9 import sys
10 import time
11 from shutil import copyfile as cp
12 import xml.etree.ElementTree as ET
13 
14 from seadasutils.netcdf_utils import ncsubset_vars
15 from seadasutils.pixlin_utils import pixlin
16 from seadasutils.setupenv import env
17 
18 def parseManifest(manifestfile):
19  tree = ET.parse(manifestfile)
20  root = tree.getroot()
21  radfiles = []
22  tiefiles = []
23  engfiles = []
24  for child in root.find('dataObjectSection'):
25  filename = pathlib.PurePath(child.find('byteStream').find('fileLocation').attrib['href']).name
26  if 'radiance' in filename:
27  radfiles.append(filename)
28  elif 'tie_' in filename:
29  tiefiles.append(filename)
30  elif 'time_coordinates' in filename:
31  continue
32  else:
33  engfiles.append(filename)
34 
35  return (radfiles, tiefiles, engfiles)
36 
37 def minmax(arr):
38  return arr.min(), arr.max()
39 
40 def epoch2000(usec):
41  # format Epoch 2000 time (microseconds since 2000-01-01)
42  base = datetime(2000, 1, 1, 0, 0, 0)
43  t = base + timedelta(microseconds=int(usec))
44  return t.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
45 
46 class extract:
47 
48  def __init__(self, idir, odir=None,
49  north=None, south=None, west=None, east=None,
50  spixl=None, epixl=None, sline=None, eline=None,
51  verbose=False):
52  # inputs
53  self.idir = pathlib.Path(idir)
54  self.odir = pathlib.Path(odir)
55  self.north = north
56  self.south = south
57  self.west = west
58  self.east = east
59  self.spixl = spixl
60  self.epixl = epixl
61  self.sline = sline
62  self.eline = eline
63  self.verbose = verbose
64  self.geofile = self.idir / 'geo_coordinates.nc'
65  self.timefile = self.idir / 'time_coordinates.nc'
66  self.tiefile = self.idir / 'tie_geo_coordinates.nc'
67  self.manifest = self.idir / 'xfdumanifest.xml'
68 
69  # defaults
70  self.runtime = None
71  self.attrs = None
72 
73  # unused, but needed by setupenv.py
74  self.dirs = {}
75  self.ancdir = None
76  self.curdir = False
77  self.sensor = None
78  env(self) # run setupenv
79 
80  def runextract(self, files, subset):
81  # subset each file
82  for filename in files:
83  srcfile = self.idir / filename
84  if srcfile.exists():
85  dstfile = self.odir / filename
86  if self.verbose:
87  print('Extracting', srcfile)
88 
89  ncsubset_vars(srcfile, dstfile, subset, timestamp=self.runtime)
90 
91  # update global attributes
92  with netCDF4.Dataset(dstfile, mode='a') as dst:
93  dst.setncatts(self.attrs)
94  return 0
95 
96  def getpixlin(self):
97  if self.verbose:
98  print("north={} south={} west={} east={}".
99  format(self.north, self.south, self.west, self.east))
100 
101  # run lonlat2pixline
102  pl = pixlin(geofile=self.geofile,
103  north=self.north, south=self.south,
104  west=self.west, east=self.east,
105  verbose=self.verbose)
106  pl.lonlat2pixline(zero=False) # using 1-based indices
107  self.spixl, self.epixl, self.sline, self.eline = \
108  (pl.spixl, pl.epixl, pl.sline, pl.eline)
109  return pl.status
110 
111  def run(self):
112  # convert to zero-based index
113  self.spixl, self.epixl, self.sline, self.eline = \
114  (v-1 for v in (self.spixl, self.epixl, self.sline, self.eline))
115 
116  # check/create output directory
117  if not self.odir:
118  self.odir = '.'.join([self.idir, 'subset'])
119  pathlib.Path(self.odir).mkdir(parents=True, exist_ok=True)
120 
121  # find tie file endpoints
122  with netCDF4.Dataset(self.tiefile, 'r') as src:
123  npixl = src.dimensions['tie_columns'].size
124  nline = src.dimensions['tie_rows'].size
125  dpixl = getattr(src, 'ac_subsampling_factor', 1) # tie_col_pts
126  dline = getattr(src, 'al_subsampling_factor', 1) # tie_row_pts
127  spixl, epixl = [self.spixl, self.epixl + dpixl - 1] // dpixl
128  sline, eline = [self.sline, self.eline + dline - 1] // dline
129 
130  # Make sure tie files have enough points for interpolation.
131  # l1_olci.c uses type gsl_interp_cspline, which requires
132  # at least 3 pixels in each dimension.
133  points_required = 3
134 
135  pixls_needed = points_required - (epixl - spixl + 1)
136  if pixls_needed > 0:
137  spixl = max(0, spixl - pixls_needed // 2)
138  epixl = min(spixl + points_required, npixl) - 1
139  if epixl == npixl - 1:
140  spixl = npixl - points_required
141 
142  lines_needed = points_required - (eline - sline + 1)
143  if lines_needed > 0:
144  sline = max(0, sline - lines_needed // 2)
145  eline = min(sline + points_required, nline) - 1
146  if eline == nline - 1:
147  sline = nline - points_required
148 
149  # scale to full-resolution coordinates
150  # will be aligned with tie files
151  spixl, epixl = [dpixl*v for v in (spixl, epixl)]
152  sline, eline = [dline*v for v in (sline, eline)]
153  if self.verbose:
154  print("spixl={} epixl={} sline={} eline={}".
155  format(spixl+1, epixl+1, sline+1, eline+1))
156 
157  # find new start, stop times
158  with netCDF4.Dataset(self.timefile, 'r') as src:
159  ts = src['time_stamp'][[sline, eline]]
160  start_time = epoch2000(ts[0])
161  stop_time = epoch2000(ts[1])
162 
163  # find new lat/lon ranges
164  with netCDF4.Dataset(self.geofile, 'r') as src:
165  lat_min, lat_max = minmax(src['latitude']
166  [sline:eline, spixl:epixl])
167  lon_min, lon_max = minmax(src['longitude']
168  [sline:eline, spixl:epixl])
169 
170  # define global attributes
171  self.attrs = {'start_time': start_time,
172  'stop_time': stop_time,
173  'geospatial_lat_min': lat_min,
174  'geospatial_lat_max': lat_max,
175  'geospatial_lon_min': lon_min,
176  'geospatial_lon_max': lon_max }
177  self.runtime = time.gmtime() # same for all files
178 
179  # extract time_coordinates
180  subset = {'rows': [sline, eline]}
181  status = self.runextract([self.timefile.name], subset)
182 
183  # extract full-resolution files
184  subset = {'columns':[spixl, epixl],
185  'rows': [sline, eline]}
186  (radfiles, tiefiles, engfiles) = parseManifest(self.manifest)
187  status += self.runextract(radfiles + engfiles, subset)
188 
189  # extract lower-resolution (tie) files
190  subset = {'tie_columns':[spixl, epixl] // dpixl,
191  'tie_rows': [sline, eline] // dline}
192  status += self.runextract(tiefiles, subset)
193 
194  return status
195 
196 
197 if __name__ == "__main__":
198 
199  # parse command line
200  parser = argparse.ArgumentParser(
201  description='Extract specified area from OLCI Level 1B files.',
202  epilog='Specify either geographic limits or pixel/line ranges, not both.')
203  parser.add_argument('-v', '--verbose', help='print status messages',
204  action='store_true')
205  parser.add_argument('idir',
206  help='directory containing OLCI Level 1B files')
207  parser.add_argument('odir', nargs='?',
208  help='output directory (defaults to "idir.subset")')
209 
210  group1 = parser.add_argument_group('geographic limits')
211  group1.add_argument('-n', '--north', type=float, help='northernmost latitude')
212  group1.add_argument('-s', '--south', type=float, help='southernmost latitude')
213  group1.add_argument('-w', '--west', type=float, help='westernmost longitude')
214  group1.add_argument('-e', '--east', type=float, help='easternmost longitude')
215 
216  group2 = parser.add_argument_group('pixel/line ranges (1-based)')
217  group2.add_argument('--spixl', type=int, help='start pixel')
218  group2.add_argument('--epixl', type=int, help='end pixel')
219  group2.add_argument('--sline', type=int, help='start line')
220  group2.add_argument('--eline', type=int, help='end line')
221 
222  if len(sys.argv) == 1:
223  parser.print_help()
224  sys.exit(1)
225  args = parser.parse_args()
226 
227  # initialize
228  this = extract(idir=args.idir,
229  odir=args.odir,
230  north=args.north,
231  south=args.south,
232  west=args.west,
233  east=args.east,
234  spixl=args.spixl,
235  epixl=args.epixl,
236  sline=args.sline,
237  eline=args.eline,
238  verbose=args.verbose)
239 
240  # file checks
241  if not this.idir.exists():
242  print("ERROR: Directory '{}' does not exist. Exiting.".format(this.idir))
243  sys.exit(1)
244  if not this.timefile.exists():
245  print("ERROR: Timestamp file ({}) not found!".format(this.timefile))
246  sys.exit(1)
247  if not this.geofile.exists():
248  print("ERROR: Geolocation file ({}) not found!".format(this.geofile))
249  sys.exit(1)
250  if not this.tiefile.exists():
251  print("ERROR: Tie file ({}) not found!".format(this.tiefile))
252  sys.exit(1)
253 
254  # input value checks
255  goodlatlons = None not in (this.north, this.south, this.west, this.east)
256  goodindices = None not in (this.spixl, this.epixl, this.sline, this.eline)
257  if (goodlatlons and goodindices):
258  print("ERROR: Specify either geographic limits or pixel/line ranges, not both.")
259  sys.exit(1)
260  elif goodlatlons:
261  status = this.getpixlin()
262  if status not in (0, 110):
263  print("No extract; lonlat2pixline status =", status)
264  exit(status)
265  elif goodindices:
266  pass
267  else:
268  print("ERROR: Specify all values for either geographic limits or pixel/line ranges.")
269  sys.exit(1)
270 
271  # run
272  status = this.run()
273 
274  # copy the manifest in case we ever need it
275  cp(this.manifest, this.odir / 'xfdumanifest.xml')
276 
277  exit(status)
def runextract(self, files, subset)
def parseManifest(manifestfile)
def env(self)
Definition: setupenv.py:7
def __init__(self, idir, odir=None, north=None, south=None, west=None, east=None, spixl=None, epixl=None, sline=None, eline=None, verbose=False)
def ncsubset_vars(srcfile, dstfile, subset, verbose=False, **kwargs)