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