NASA Logo
Ocean Color Science Software

ocssw V2022
l1info_harp2.py
Go to the documentation of this file.
1 #! /usr/bin/env python3
2 
3 # l1info for HARP2 L1B. Returns the following information:
4 # 1. Start Date and Time
5 # 2. End Date and Time
6 # 3. Gring information (lon-lats) that encloses all possible lon,lat pairs for the file.
7 # Return in clock-wise order.
8 # 4. Geospatial bounds. The same a Gring, but in counter-clockwise order.
9 #
10 # Return codes:
11 # 0 if no issues detected
12 # 100 if bad file type
13 # 110 if there's bad gring order
14 # 120 if there's bad geospatial bounds order
15 # 130 bad file, missing more than 1 corner of the polygon
16 
17 
18 import argparse
19 import numpy as np
20 from netCDF4 import Dataset as NetCDF
21 
22 # year, month, day
23 __version__ = "2.1 (2024-09-18)"
24 
25 
26 # Class that holds lon and lat points
28 
29  # constructor
30  def __init__(self, lon, lat):
31  self.lon = lon
32  self.lat = lat
33 
34 
35 # Class to store the most east and most west point of a single Harp2 Line
37  def __init__(self, west:LonLatPoint, east:LonLatPoint):
38  self.west = west
39  self.east = east
40 
41 
42 # Good gring is in clockwise order.
43 def IsGoodGring(lons, lats):
44 
45  # append first element to the end to make polygon closed
46  lons.append(lons[0])
47  lats.append(lats[0])
48  lons = list(reversed(lons))
49  lats = list(reversed(lats))
50  area = CalculatePolygonArea(lons, lats)
51  if area < 0: # negative == clockwise order
52  return True
53  return False
54 
55 
56 
57 # Good geospatial bounds is in counter-clockwise order.
58 def IsGoodGeospatialBounds(lons, lats):
59 
60  area = CalculatePolygonArea(lons, lats)
61  if area > 0: # positive area == counter-clockwise order.
62  return True
63  return False
64 
65 
66 
67 
68 # Return the area of a closed polygon from a list of points.
69 # @param points - list of Coordinate objects
70 # @ return area
71 #
72 # Code based on the formula from: https://mathworld.wolfram.com/PolygonArea.html
73 #
74 # Area:
75 # Negative (-) points arranged in clockwise order
76 # Positive (+) points arranged in counter-clockwise order
77 #
78 # The formula is made for a 2D plane. Using Longitude and Latitude,
79 
80 def CalculatePolygonArea(lons, lats):
81  runningSum = 0
82  for i in range(len(lons)-1):
83 
84  x1 = lons[i]
85  y1 = lats[i]
86 
87  x2 = lons[i+1]
88  y2 = lats[i+1]
89 
90  diff = (x1*y2) - (x2*y1)
91  runningSum += diff
92 
93  return runningSum
94 
95 
96 # True if the view and line has at least 2 pixels that are not fill value
97 # False otherwise.
98 def Has2GoodPixels(view, line):
99  global lonData
100  global latData
101  global numPixels
102 
103  goodPixelCount = 0
104 
105  for pix in range(numPixels):
106 
107  # already seen 2, return and stop checking for more
108  if (goodPixelCount == 2):
109  return True
110  if (~lonData[view][line].mask[pix] and ~latData[view][line].mask[pix]):
111  goodPixelCount+=1
112 
113  return False
114 
115 
116 # True if the line has is not all fill values. False otherwise.
117 def LineNotAllMaskValues(view, line):
118  global lonData
119  global latData
120  return np.any(~lonData[view][line].mask) and np.any(~latData[view][line].mask)
121 
122 # Have a pointer at the start and end of the views line.
123 # Move the start pointer forward if it contains fill values and move the end
124 # pointer backwards if it contains fill values.
125 # return the first good line and last good line when both are good
127 
128  global lonData
129  global latData
130  global numLines
131 
132  firstGoodLine = 0
133  lastGoodLine = numLines-1
134 
135  # make sure they dont cross when searching
136  while (firstGoodLine < lastGoodLine):
137 
138  # check if the line is not all fill value
139  isGoodFirstLine = LineNotAllMaskValues(view, firstGoodLine)
140  isGoodLastLine = LineNotAllMaskValues(view, lastGoodLine)
141 
142  # both good, then check that it has at least 2 good pixels
143  if (isGoodFirstLine and isGoodLastLine):
144 
145  goodFirstLine = Has2GoodPixels(view, firstGoodLine)
146  goodLastLine = Has2GoodPixels(view, lastGoodLine)
147 
148  # both good
149  if (goodFirstLine and goodLastLine):
150  break
151  # only first line has at least 2 pixels
152  elif (goodFirstLine and not goodLastLine):
153  lastGoodLine-= 1
154  # only good last line
155  elif (not goodFirstLine and goodLastLine):
156  firstGoodLine +=1
157  else:
158  firstGoodLine+=1
159  lastGoodLine-=1
160 
161  # good first, bad last, move only the last pointer
162  elif (isGoodFirstLine and not isGoodLastLine):
163  lastGoodLine-=1
164  elif (not isGoodFirstLine and isGoodLastLine):
165  firstGoodLine+=1
166  else:
167  firstGoodLine+=1
168  lastGoodLine-=1
169 
170  return firstGoodLine, lastGoodLine
171 
172 
173 # Determines if there are at least 2 good lines in the view
175 
176  global lonData
177  global latData
178  global numLines
179 
180  firstGoodLine = 0
181  lastGoodLine = numLines-1
182 
183  # make sure they dont cross when searching
184  while (firstGoodLine < lastGoodLine):
185 
186  # check if the line is not all fill value
187  isGoodFirstLine = LineNotAllMaskValues(view, firstGoodLine)
188  isGoodLastLine = LineNotAllMaskValues(view, lastGoodLine)
189 
190  # both good, then check that it has at least 2 good pixels
191  if (isGoodFirstLine and isGoodLastLine):
192 
193  goodFirstLine = Has2GoodPixels(view, firstGoodLine)
194  goodLastLine = Has2GoodPixels(view, lastGoodLine)
195 
196  # both good
197  if (goodFirstLine and goodLastLine):
198  break
199  # only first line has at least 2 pixels
200  elif (goodFirstLine and not goodLastLine):
201  lastGoodLine-= 1
202  # only good last line
203  elif (not goodFirstLine and goodLastLine):
204  firstGoodLine +=1
205  else:
206  firstGoodLine+=1
207  lastGoodLine-=1
208 
209  # good first, bad last, move only the last pointer
210  elif (isGoodFirstLine and not isGoodLastLine):
211  lastGoodLine-=1
212  elif (not isGoodFirstLine and isGoodLastLine):
213  firstGoodLine+=1
214  else:
215  firstGoodLine+=1
216  lastGoodLine-=1
217 
218  # case where is there no good lines or when there is only 1 line
219  if (firstGoodLine >= lastGoodLine):
220  return False
221 
222  return True
223 
224 # False if it contains masked values, true otherwise
225 def CheckPixelForNoMask(view, line, pixel):
226  global lonData
227  global latData
228  return np.any(~lonData[view][line].mask[pixel]) and np.any(~latData[view][line].mask[pixel])
229 
230 
231 # Given a view number, get the west and east points for a north or south location
232 def GetWestEastPointsForView(view, isNorth):
233 
234  global lonData
235  global latData
236  global numLines
237  global numPixels
238 
239  # get first and last good lines of a view. The first line will contain the most
240  # southern pixel and the last line will contain the most northern pixel
241  firstLine, lastLine = GetFirstAndLastGoodLinesForView(view)
242 
243  # if firstLine and lastLine is false, then this view is not good.
244  # try a different view
245 
246  try:
247  # isNorth == true, then use the last line. Otherwise, use the first line for south
248  line = lastLine if isNorth else firstLine
249 
250  # assume the first good pixel is most left and last good pixel is most right
251  # move the pointers forward or backwards if they are fill values
252  firstGoodPixel = 0
253  lastGoodPixel = numPixels-1
254 
255  # make sure they dont cross when searching for first and last good pixel
256  while (firstGoodPixel < lastGoodPixel):
257 
258  # get the status of the pixels
259  isGoodFirstPixel = CheckPixelForNoMask(view, line, firstGoodPixel)
260  isGoodLastPixel = CheckPixelForNoMask(view, line, lastGoodPixel)
261 
262  # both good, dont check anymore and exit while loop
263  if (isGoodFirstPixel and isGoodLastPixel):
264  break
265 
266  # good first pixel, but bad last pixel, move only the last pointer
267  elif (isGoodFirstPixel and not isGoodLastPixel):
268  lastGoodPixel-=1
269 
270  # bad first pixel, but good last pixel, move only the first pointer
271  elif (not isGoodFirstPixel and isGoodLastPixel):
272  firstGoodPixel+=1
273 
274  # both bad, move both pointers
275  else:
276  firstGoodPixel+=1
277  lastGoodPixel-=1
278 
279 
280  # if no issues, make the lon, lat pairs
281  leftPoint = LonLatPoint(lonData[view][line][firstGoodPixel], latData[view][line][firstGoodPixel])
282  rightPoint = LonLatPoint(lonData[view][line][lastGoodPixel], latData[view][line][lastGoodPixel])
283 
284  # store this line's lon and lat in a coordinate class
285  # Harp's left most pixel of a line is east and right most pixel is west
286  # Harp's first line of the file is most south and last line is most north
287  coordinateObj = Coordinates(east=rightPoint, west=leftPoint)
288 
289  return coordinateObj
290 
291  # line returns None for first and last line. It will cause an exception. Catch it
292  # and return Coordinates that are fill values.
293  except Exception as e:
294  print(e)
295  return Coordinates(east=LonLatPoint(-999, -999), west=LonLatPoint(-999, -999))
296 
297 
298 # From a list of views, get the lon and lat data for west and east side of the view
299 # and return a dictionary where the key is the view number and the value is a Coordinate
300 # object that contains west and east LonLatPoint objects
301 def GetLonLatPointForViewsAsDict(viewsList, isNorth):
302  viewCoordinates = {}
303  for view in viewsList:
304  coordinate = GetWestEastPointsForView(view=view, isNorth=isNorth)
305  viewCoordinates[view] = coordinate
306 
307  return viewCoordinates
308 
309 
310 # Make a gring in clockwise order
311 # northCoordinates is a dict with view numbers as keys and a Coordinate object as its
312 # value. Same for southCoordinates
313 def ConstructGring(northCoordinates, southCoordinates):
314  # gring is in clockwise order, do get the lon and lat data in this order based on the
315  # static CORNER_VIEWS variables:
316 
317  # (1) NW Views ---------------------------- NE Views (2)
318  # | |
319  # | |
320  # (4) reversed(SW View) --------- reversed(SE Views) (3)
321 
322  global NW_CORNER_VIEWS
323  global NE_CORNER_VIEWS
324  global SW_CORNER_VIEWS
325  global SE_CORNER_VIEWS
326 
327  gringLons = []
328  gringLats = []
329 
330  # (1) North West Points
331  for view in NW_CORNER_VIEWS:
332  gringLons.append(northCoordinates[view].west.lon)
333  gringLats.append(northCoordinates[view].west.lat)
334 
335  # (2) North East Points
336  for view in NE_CORNER_VIEWS:
337  gringLons.append(northCoordinates[view].east.lon)
338  gringLats.append(northCoordinates[view].east.lat)
339 
340  # (3) South East Points
341  for view in reversed(SE_CORNER_VIEWS):
342  gringLons.append(southCoordinates[view].east.lon)
343  gringLats.append(southCoordinates[view].east.lat)
344 
345  # (4) South West Points
346  for view in reversed(SW_CORNER_VIEWS):
347  gringLons.append(southCoordinates[view].west.lon)
348  gringLats.append(southCoordinates[view].west.lat)
349 
350  # check that it is in clockwise order
351 
352  return gringLons, gringLats
353 
354 
355 
356 # Make Geospatial Bounds in **Counter Clockwise** Order
357 # northCoordinates is a dict with view numbers as keys and a Coordinate object as its
358 # value. Same for southCoordinates
359 def ConstructGeospatialBounds(northCoordinates, southCoordinates):
360  # gring is in clockwise order, do get the lon and lat data in this order based on the
361  # static CORNER_VIEWS variables:
362 
363  # (1) reversed(NW Views) -------- reversed(NE Views) (4)
364  # | |
365  # | |
366  # (2) SW View ----------------------------- SE Views (3)
367 
368  global NW_CORNER_VIEWS
369  global NE_CORNER_VIEWS
370  global SW_CORNER_VIEWS
371  global SE_CORNER_VIEWS
372 
373  lons = []
374  lats = []
375 
376  # (1) North West Points
377  for view in reversed(NW_CORNER_VIEWS):
378  lons.append(northCoordinates[view].west.lon)
379  lats.append(northCoordinates[view].west.lat)
380 
381  # (2) South West Points
382  for view in SW_CORNER_VIEWS:
383  lons.append(southCoordinates[view].west.lon)
384  lats.append(southCoordinates[view].west.lat)
385  # (3) South East Points
386  for view in SE_CORNER_VIEWS:
387  lons.append(southCoordinates[view].east.lon)
388  lats.append(southCoordinates[view].east.lat)
389 
390  # (3) North East Points
391  for view in reversed(NE_CORNER_VIEWS):
392  lons.append(northCoordinates[view].east.lon)
393  lats.append(northCoordinates[view].east.lat)
394 
395  return lons, lats
396 
397 
398  # Prints the files Gring information. Gring is a non-closed polygon and it prints
399 # in clockwise direction.
400 # Point == tuple(lon, lat)
401 
402 def PrintGring(lons, lats):
403 
404  gringLonStr = "gringpointlongitude="
405  gringLatStr = "gringpointlatitude="
406  # 1 less point because last point is duplicated. gring doesnt need to be closed
407  for i in range(len(lons)):
408 
409  # dont print bad points
410  if (lons[i] == -999 or lats[i] == -999):
411  continue
412  gringLonStr += f"{lons[i]},"
413  gringLatStr += f"{lats[i]},"
414 
415  # delete the last "," from the final lon and lat string
416  gringLonStr = gringLonStr[:-1]
417  gringLatStr = gringLatStr[:-1]
418 
419  print(gringLonStr)
420  print(gringLatStr)
421  print(f"gringpointsequence=1,2,3,4")
422 
423 
424 
425 
426 # Prints the files Geospatial Bounds information. Geospatial Bounds is a closed polygon, where
427 # the first point is repeated.
428 #
429 # It prints in counter-clockwise direction from the corners:
430 # NOTE: upper right repeated to make closed polygon
431 # upper right, upper left, lower left, lower right, upper right
432 #
433 # Format is different from gring:
434 # 1. space to separate lat,lon value pairs
435 # 2. comma between pairs
436 # ie:
437 # lat lon, lat lon, lat lon, ... lat lon
438 
439 def PrintGeospatialBounds(lons, lats):
440 
441  geospatialBoundsStr ="geospatial_bounds="
442 
443  for i in range(len(lons)):
444  if (lons[i] == -999 or lats[i] == -999):
445  continue
446  geospatialBoundsStr += f"{lats[i]} {lons[i]},"
447 
448  # remove last comma from string
449  geospatialBoundsStr = geospatialBoundsStr[:-1]
450 
451  print(geospatialBoundsStr)
452 
453 
454 # Given all good views that are ordered from West to East, get 4 points from them
455 def Get2GoodViews(allViews):
456  leftPoint = 0
457  rightPoint = len(allViews)-1
458 
459  while(leftPoint < rightPoint):
460  goodLeftPoint = HasGoodFirstAndLastLineForView(allViews[leftPoint])
461  goodRightPoint = HasGoodFirstAndLastLineForView(allViews[rightPoint])
462 
463  # both good
464  if (goodLeftPoint and goodRightPoint):
465  goodLeftViewNum = allViews[leftPoint]
466  goodRightViewNum = allViews[rightPoint]
467  return [goodLeftViewNum, goodRightViewNum]
468 
469  # right side is bad
470  elif (goodLeftPoint and not goodRightPoint):
471  rightPoint-=1
472  # left side is bad
473  elif (not goodLeftPoint and goodRightPoint):
474  leftPoint += 1
475  # both bad
476  else:
477  leftPoint+=1
478  rightPoint-=1
479 
480  # end of while loop, none is found, this is a bad file without good views to
481  # make a polygon
482  global emptyCornerCount
483  emptyCornerCount+=1
484  return []
485 
486 
487 
488 
489 def main():
490  print("l1info_harp2", __version__)
491  global status
492  status = 0
493 
494  # open the netcdf file and extract global variables
495  helpMessage = """Given a Harp2 L1B file, return time, gring and geospatial bounds. \n
496  Status Codes:
497  0: Everything is fine,
498  100: Wrong file type
499  110: Bad Geolocation,
500  120: Bad Geospacial bounds,
501  130: Bad file, missing more than 1 corner of the polygon or wrong file.
502  """
503 
504  # add the help message when users do not give any command line arguments
505  parser = argparse.ArgumentParser(
506  description=helpMessage,
507 
508  # required to keep helpMessage tab and space formatting. Otherwise, it's 1 long string
509  formatter_class=argparse.RawDescriptionHelpFormatter
510  )
511  parser.add_argument("iFile", type=str, help="HARP2 L1B NetCDF file")
512  args = parser.parse_args()
513 
514  # check that it is a L1B file. L1B for HARP2 contains geolocation data
515  # file format is PACE_OCI.date.fileLevel. Slice by "." and index 2 is the file level
516  if (args.iFile.split(".")[2].upper() != "L1B"):
517  print("Input file is not L1B. Exiting...")
518  return 100
519 
520  ncFile = NetCDF(args.iFile, "r+", format="NETCDF4")
521 
522  # get start and end time
523  start_time = ncFile.__dict__["time_coverage_start"]
524  end_time = ncFile.__dict__["time_coverage_end"]
525 
526  # load lon and lat data. Make it global for easy access to other functions
527  global lonData
528  global latData
529  lonData = ncFile.groups["geolocation_data"]["longitude"][:]
530  latData = ncFile.groups["geolocation_data"]["latitude"][:]
531 
532 
533  # shape of the dataset. make it global so other functions can access
534  global numViews, numLines, numPixels
535  numViews = len(ncFile.dimensions["views"])
536  numLines = len(ncFile.dimensions["swath_lines"])
537  numPixels = len(ncFile.dimensions["swath_pixels"])
538 
539 
540 
561 
562 
566  global ALL_NE_VIEWS, ALL_NE_VIEWS, ALL_SW_VIEWS, ALL_SE_VIEWS
567  ALL_NW_VIEWS = [17, 71, 16, 15, 14, 13, 12, 14, 13, 12, 80, 11, 70, 10]
568  ALL_NE_VIEWS = [10, 70, 11, 80, 12, 13, 14, 15, 16, 71, 17, 81, 18, 1, 19, 20, 21, 22]
569  ALL_SW_VIEWS = [62, 63, 64, 65, 66, 67, 79, 68]
570  ALL_SE_VIEWS = [79, 67, 9, 66, 65, 64, 63, 62, 78, 61, 88, 59, 58, 57, 56]
571 
572  # increment when getting 2 good views. if it is empty, increment this.
573  # if 2 or more, this is a bad file because it will be missing too may corners
574  global emptyCornerCount
575  emptyCornerCount = 0
576 
577  # Views to grab to make the NorthWest (NW) corners of the gring. Each consecutive view will
578  # have a lon, lat pair than is more east than the previous view.
579  global NW_CORNER_VIEWS
580  NW_CORNER_VIEWS = Get2GoodViews(ALL_NW_VIEWS)
581  #NW_CORNER_VIEWS = [17, 15, 13, 10]
582 
583  # views for the NE corner. Each consecutive view will also have a lon, lat pair
584  # than is more east than the previous view
585  global NE_CORNER_VIEWS
586  NE_CORNER_VIEWS = Get2GoodViews(ALL_NE_VIEWS)
587  #NE_CORNER_VIEWS = [10, 15, 18, 22]
588 
589  # SW and SE views. Following the same logic as NW and NE
590  global SW_CORNER_VIEWS
591  SW_CORNER_VIEWS = Get2GoodViews(ALL_SW_VIEWS)
592  #SW_CORNER_VIEWS = [62, 64, 67, 68]
593 
594  global SE_CORNER_VIEWS
595  SE_CORNER_VIEWS = Get2GoodViews(ALL_SE_VIEWS)
596  #SE_CORNER_VIEWS = [79, 63, 58, 56]
597 
598 
599  # missing corners, it is a bad file
600  if emptyCornerCount > 0:
601  status = 130
602 
603 
604  # for each of the north views, grab the lon and lat data and save it to a map
605  # it will be used to construct the gring and geospatial bounds and so that each
606  # view's coordinates are not calculated twice
607  northViews = list(set(NW_CORNER_VIEWS + NE_CORNER_VIEWS)) # unique views to north side of map
608  northViewCoordinates = GetLonLatPointForViewsAsDict(viewsList=northViews, isNorth=True)
609 
610  # do the same for the south views
611  southViews = list(set(SW_CORNER_VIEWS + SE_CORNER_VIEWS)) #
612  southViewCoordinates = GetLonLatPointForViewsAsDict(viewsList=southViews, isNorth=False)
613 
614 
615 
617  gringLons, gringLats = ConstructGring(northViewCoordinates, southViewCoordinates)
618 
619 
620 
622  geospatialLons, geospatialLats = ConstructGeospatialBounds(northViewCoordinates, southViewCoordinates)
623 
624 
625 
627 
628 
629 
630 
631  print(f"Start_Date={start_time[:10]}")
632  print(f"Start_Time={start_time[11:-1]}")
633  print(f"End_Date={end_time[:10]}")
634  print(f"End_Time={end_time[11:-1]}")
635 
636 
637  PrintGring(gringLons, gringLats)
638 
639 
640  PrintGeospatialBounds(geospatialLons, geospatialLats)
641 
642  return status
643 
644 
645 if __name__ == "__main__":
646  main()
def GetFirstAndLastGoodLinesForView(view)
def IsGoodGeospatialBounds(lons, lats)
Definition: l1info_harp2.py:58
def Has2GoodPixels(view, line)
Definition: l1info_harp2.py:98
def Get2GoodViews(allViews)
def ConstructGeospatialBounds(northCoordinates, southCoordinates)
def CalculatePolygonArea(lons, lats)
Definition: l1info_harp2.py:80
def main()
The view numbers are the best views that gets the corners of HARP2's lon and lat data.
def IsGoodGring(lons, lats)
Definition: l1info_harp2.py:43
list(APPEND LIBS ${NETCDF_LIBRARIES}) find_package(GSL REQUIRED) include_directories($
Definition: CMakeLists.txt:8
def __init__(self, LonLatPoint west, LonLatPoint east)
Definition: l1info_harp2.py:37
def ConstructGring(northCoordinates, southCoordinates)
void print(std::ostream &stream, const char *format)
Definition: PrintDebug.hpp:38
def HasGoodFirstAndLastLineForView(view)
def CheckPixelForNoMask(view, line, pixel)
def __init__(self, lon, lat)
Definition: l1info_harp2.py:30
def PrintGeospatialBounds(lons, lats)
def GetLonLatPointForViewsAsDict(viewsList, isNorth)
def GetWestEastPointsForView(view, isNorth)
def LineNotAllMaskValues(view, line)
def PrintGring(lons, lats)
while(++r<=NROOTS)
Definition: decode_rs.h:169