import os import numpy as np # # 2026-03-19 # Land, Vegetation, and Ice (LVIS) Team # http://lvis.gsfc.nasa.gov/ # # EXAMPLE USAGE: # from read_lvis2 import read_lvis2 # data = read_lvis2(lvis2_file) # data = read_lvis2(lvis2_file, quiet=True) # def read_lvis2(file, quiet=False): """ Python code to read in all LVIS Level-2 release files v1.04, v1.05, v2.0.1, v2.0.2, v2.0.3, v2.0.4, v2.0.5 Parameters ---------- file : str L2 .TXT file to read in. quiet : bool, optional If False (default), prints status messages. Returns ------- data : numpy structured array Data returned as a NumPy structured array, or None on failure. """ # ------------------------------------------------------------------------- # Define numpy dtypes for each format version. # ------------------------------------------------------------------------- # v1.04: Ice surfaces 2009-2015 (IceBridge) [12 columns] dtype_v104 = np.dtype([ ('lvis_lfid', np.uint32), # LVIS file identification ('shotnumber', np.uint32), # Laser shot assigned during collection ('time', np.float64), # UTC decimal seconds of the day ('longitude_centroid', np.float64), # Centroid longitude of the Level-1B waveform ('latitude_centroid', np.float64), # Centroid latitude of the Level-1B waveform ('elevation_centroid', np.float64), # Centroid elevation of the Level-1B waveform ('longitude_low', np.float64), # Longitude of the lowest detected mode ('latitude_low', np.float64), # Latitude of the lowest detected mode ('elevation_low', np.float64), # Mean elevation of the lowest detected mode ('longitude_high', np.float64), # Longitude of the highest mode ('latitude_high', np.float64), # Latitude of the highest mode ('elevation_high', np.float64), # Elevation of the highest mode ]) # v1.05: Land surfaces 1997-2009 rereleased [17 columns] dtype_v105 = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('date', np.int32), ('time', np.float64), ('glon', np.float64), ('glat', np.float64), ('zg', np.float32), ('tlon', np.float64), ('tlat', np.float64), ('zt', np.float32), ('rh25', np.float32), ('rh50', np.float32), ('rh75', np.float32), ('rh100', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ]) # v2.0.1: Gabon 2016 (2nd release 2018) & Greenland 2017 [42 columns] dtype_v201 = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('time', np.float64), ('glon', np.float64), ('glat', np.float64), ('zg', np.float32), ('hlon', np.float64), # removed for v2.0.2 ('hlat', np.float64), # removed for v2.0.2 ('zh', np.float32), # removed for v2.0.2 ('tlon', np.float64), ('tlat', np.float64), ('zt', np.float32), ('rh10', np.float32), ('rh15', np.float32), ('rh20', np.float32), ('rh25', np.float32), ('rh30', np.float32), ('rh35', np.float32), ('rh40', np.float32), ('rh45', np.float32), ('rh50', np.float32), ('rh55', np.float32), ('rh60', np.float32), ('rh65', np.float32), ('rh70', np.float32), ('rh75', np.float32), ('rh80', np.float32), ('rh85', np.float32), ('rh90', np.float32), ('rh95', np.float32), ('rh96', np.float32), ('rh97', np.float32), ('rh98', np.float32), ('rh99', np.float32), ('rh100', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ('complexity', np.float32), ('channel_zt', np.int16), ('channel_zg', np.int16), ('channel_rh', np.int16), ]) # defunct: Gabon 2016 first release [38 columns] dtype_defunct = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('time', np.float64), ('glon', np.float64), ('glat', np.float64), ('zg', np.float32), ('tlon', np.float64), ('tlat', np.float64), ('zt', np.float32), ('rh10', np.float32), ('rh15', np.float32), ('rh20', np.float32), ('rh25', np.float32), ('rh30', np.float32), ('rh35', np.float32), ('rh40', np.float32), ('rh45', np.float32), ('rh50', np.float32), ('rh55', np.float32), ('rh60', np.float32), ('rh65', np.float32), ('rh70', np.float32), ('rh75', np.float32), ('rh80', np.float32), ('rh85', np.float32), ('rh90', np.float32), ('rh95', np.float32), ('rh96', np.float32), ('rh97', np.float32), ('rh98', np.float32), ('rh99', np.float32), ('rh100', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ('channel_zt', np.int16), ('channel_zg', np.int16), ('channel_rh', np.int16), ]) # v2.0.2: ABoVE 2017 [39 columns] dtype_v202 = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('time', np.float64), ('glon', np.float64), ('glat', np.float64), ('zg', np.float32), ('tlon', np.float64), ('tlat', np.float64), ('zt', np.float32), ('rh10', np.float32), ('rh15', np.float32), ('rh20', np.float32), ('rh25', np.float32), ('rh30', np.float32), ('rh35', np.float32), ('rh40', np.float32), ('rh45', np.float32), ('rh50', np.float32), ('rh55', np.float32), ('rh60', np.float32), ('rh65', np.float32), ('rh70', np.float32), ('rh75', np.float32), ('rh80', np.float32), ('rh85', np.float32), ('rh90', np.float32), ('rh95', np.float32), ('rh96', np.float32), ('rh97', np.float32), ('rh98', np.float32), ('rh99', np.float32), ('rh100', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ('complexity', np.float32), ('channel_zt', np.int16), ('channel_zg', np.int16), ('channel_rh', np.int16), ]) # v2.0.3: Land surfaces 2018+ [43 columns] dtype_v203 = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('time', np.float64), ('glon', np.float64), ('glat', np.float64), ('zg', np.float32), ('hlon', np.float64), ('hlat', np.float64), ('zh', np.float32), ('tlon', np.float64), ('tlat', np.float64), ('zt', np.float32), ('rh10', np.float32), ('rh15', np.float32), ('rh20', np.float32), ('rh25', np.float32), ('rh30', np.float32), ('rh35', np.float32), ('rh40', np.float32), ('rh45', np.float32), ('rh50', np.float32), ('rh55', np.float32), ('rh60', np.float32), ('rh65', np.float32), ('rh70', np.float32), ('rh75', np.float32), ('rh80', np.float32), ('rh85', np.float32), ('rh90', np.float32), ('rh95', np.float32), ('rh96', np.float32), ('rh97', np.float32), ('rh98', np.float32), ('rh99', np.float32), ('rh100', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ('complexity', np.float32), ('sensitivity', np.float32), ('channel_zt', np.int16), ('channel_zg', np.int16), ('channel_rh', np.int16), ]) # v2.0.4: Ice surfaces 2022+ [24 columns] dtype_v204 = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('time', np.float64), ('lonlo', np.float64), ('latlo', np.float64), ('zlo', np.float32), ('lonmx', np.float64), ('latmx', np.float64), ('zmx', np.float32), ('lonhi', np.float64), ('lathi', np.float64), ('zhi', np.float32), ('lonlo_alt', np.float64), ('latlo_alt', np.float64), ('zlo_alt', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ('complexity', np.float32), ('sensitivity', np.float32), ('energy1', np.float32), ('energy2', np.float32), ('energy3', np.float32), ('bestwave', np.int16), ]) # v2.0.5: BioSCape 2023 [45 columns] dtype_v205 = np.dtype([ ('lfid', np.uint32), ('shotnumber', np.uint32), ('time', np.float64), ('glon', np.float64), ('glat', np.float64), ('zg', np.float32), ('zg_alt1', np.float32), # v2.0.5 ('zg_alt2', np.float32), # v2.0.5 ('hlon', np.float64), ('hlat', np.float64), ('zh', np.float32), ('tlon', np.float64), ('tlat', np.float64), ('zt', np.float32), ('rh10', np.float32), ('rh15', np.float32), ('rh20', np.float32), ('rh25', np.float32), ('rh30', np.float32), ('rh35', np.float32), ('rh40', np.float32), ('rh45', np.float32), ('rh50', np.float32), ('rh55', np.float32), ('rh60', np.float32), ('rh65', np.float32), ('rh70', np.float32), ('rh75', np.float32), ('rh80', np.float32), ('rh85', np.float32), ('rh90', np.float32), ('rh95', np.float32), ('rh96', np.float32), ('rh97', np.float32), ('rh98', np.float32), ('rh99', np.float32), ('rh100', np.float32), ('azimuth', np.float32), ('incidentangle', np.float32), ('range', np.float32), ('complexity', np.float32), ('sensitivity', np.float32), ('channel_zt', np.int16), ('channel_zg', np.int16), ('channel_rh', np.int16), ]) # Map number of columns -> (dtype, version label) dtype_map = { 12: (dtype_v104, 'LDS v1.04: Ice surfaces 2009-2015 (IceBridge)'), 17: (dtype_v105, 'LDS v1.05: Land surfaces 1997-2009 rereleased'), 42: (dtype_v201, 'LDS v2.0.1: Gabon 2016 (2nd release 2018) & Greenland 2017'), 38: (dtype_defunct, 'OUTDATED FILES; download new ones here: https://search.earthdata.nasa.gov/search?q=AFLVIS2'), 39: (dtype_v202, 'LDS v2.0.2: ABoVE 2017'), 43: (dtype_v203, 'LDS v2.0.3: Land surfaces 2018+'), 24: (dtype_v204, 'LDS v2.0.4: Ice surfaces 2022+'), 45: (dtype_v205, 'LDS v2.0.5: BioSCape 2023'), } # ------------------------------------------------------------------------- # File check # ------------------------------------------------------------------------- if not os.path.isfile(file): print(f'File not found: {file}') return None infile = file if not quiet: print(f'Reading in LVIS LEVEL-2 file: {infile}') # ------------------------------------------------------------------------- # First pass: separate header lines from data lines and detect column count # ------------------------------------------------------------------------- header_lines = [] first_data_line = None ntags = None with open(infile, 'r') as fh: for line in fh: stripped = line.rstrip('\n').rstrip('\r') if not stripped: # Treat blank lines before data as header lines header_lines.append(stripped) continue if stripped[0].isdigit(): # First character is a digit -> first data line first_data_line = stripped ntags = len(stripped.split()) break else: # Does not start with a digit -> header line header_lines.append(stripped) if first_data_line is None: print(f'This is not an LVIS Level-2 release file: {infile}') return None # ------------------------------------------------------------------------- # Identify format version by column count # ------------------------------------------------------------------------- if ntags not in dtype_map: print(f'This file does not have the standard number of columns ' f'for an LVIS Level-2 release file: {infile}') print(f'sample line : {first_data_line}') print(f'# of columns : {ntags}') return None chosen_dtype, version_label = dtype_map[ntags] if not quiet: print(version_label) # ------------------------------------------------------------------------- # Second pass: load all data lines with numpy, then cast into structured array # ------------------------------------------------------------------------- nhdr = len(header_lines) # np.loadtxt reads everything as float64 by default; we cast per-field below raw = np.loadtxt(infile, skiprows=nhdr, dtype=np.float64) # Handle edge case of a single data row (loadtxt returns 1-D array) if raw.ndim == 1: raw = raw.reshape(1, -1) # Build structured array and cast each column to its declared type data = np.empty(raw.shape[0], dtype=chosen_dtype) for i, field_name in enumerate(chosen_dtype.names): data[field_name] = raw[:, i].astype(chosen_dtype[field_name]) return data # ============================================================================= # Example usage # ============================================================================= if __name__ == '__main__': # Provide path to your LVIS Level 2 .TXT file here lvis_file = 'LVIS_file.TXT' # Retrieve data data = read_lvis2(lvis_file) # Retrieve data silently data = read_lvis2(lvis_file, quiet=True) if data is not None: print(f'Records read: {len(data)}') print(f'First record: {data[0]}')