diff --git a/README.md b/README.md index 683bd33..8b43cd0 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,12 @@ several instruments from Mars orbiters are supported, but the system is designed to be extensible to other instruments and bodies. Please refer to the [documentation](https://jplmlia.github.io/pdsc/) for -instructions on installation, setup, and usage. +instructions on installation, setup, and usage. --- -Copyright 2019, by the California Institute of Technology. ALL RIGHTS RESERVED. +Copyright 2023, by the California Institute of Technology. ALL RIGHTS RESERVED. United States Government Sponsorship acknowledged. Any commercial use must be negotiated with the Office of Technology Transfer at the California Institute of Technology. + diff --git a/pdsc/config/lroc_cdr_metadata.yaml b/pdsc/config/lroc_cdr_metadata.yaml new file mode 100644 index 0000000..79ba793 --- /dev/null +++ b/pdsc/config/lroc_cdr_metadata.yaml @@ -0,0 +1,49 @@ +columns: + - [PRODUCT_ID, observation_id, text] + - [LINE_SAMPLES, samples, integer] + - [IMAGE_LINES, lines, integer] + - [CENTER_LATITUDE, center_latitude, real] + - [CENTER_LONGITUDE, center_longitude, real] + - [NORTH_AZIMUTH, north_azimuth, real] + - [SCALED_PIXEL_WIDTH, pixel_width, real] + - [VOLUME_ID, volume_id, text] + - [FILE_SPECIFICATION_NAME, file_specification_name, text] + - [PRODUCT_VERSION_ID, product_version_id, text] + - [TARGET_NAME, target_name, text] + - [ORBIT_NUMBER, orbit_number, integer] + - [MISSION_PHASE_NAME, mission_phase_name, text] + - [RATIONALE_DESC, description, text] + - [START_TIME, start_time, text] + - [STOP_TIME, stop_time, text] + - [SPACECRAFT_CLOCK_START_COUNT, sclk_start, real] + - [SPACECRAFT_CLOCK_STOP_COUNT, sclk_stop, real] + - [EMISSION_ANGLE, emission_angle, real] + - [INCIDENCE_ANGLE, incidence_angle, real] + - [PHASE_ANGLE, phase_angle, real] + - [SPACECRAFT_ALTITUDE, spacecraft_altitude, real] + - [TARGET_CENTER_DISTANCE, target_center_distance, real] + - [SUB_SOLAR_AZIMUTH, sub_solar_azimuth, real] + - [SUB_SOLAR_LATITUDE, sub_solar_latitude, real] + - [SUB_SOLAR_LONGITUDE, sub_solar_longitude, real] + - [SUB_SPACECRAFT_LATITUDE, sub_spacecraft_latitude, real] + - [SUB_SPACECRAFT_LONGITUDE, sub_spacecraft_longitude, real] + - [SOLAR_DISTANCE, solar_distance, real] + - [SOLAR_LONGITUDE, solar_longitude, real] + - [UPPER_RIGHT_LATITUDE, upper_right_latitude, real] + - [UPPER_RIGHT_LONGITUDE, upper_right_longitude, real] + - [LOWER_RIGHT_LATITUDE, lower_right_latitude, real] + - [LOWER_RIGHT_LONGITUDE, lower_right_longitude, real] + - [UPPER_LEFT_LATITUDE, upper_left_latitude, real] + - [UPPER_LEFT_LONGITUDE, upper_left_longitude, real] + - [LOWER_LEFT_LATITUDE, lower_left_latitude, real] + - [LOWER_LEFT_LONGITUDE, lower_left_longitude, real] + +scale_factors: + SPACECRAFT_ALTITUDE: 1000 # km to m + SOLAR_DISTANCE: 1000 # km to m + +index: + - observation_id + +segmentation: + resolution: 50000 diff --git a/pdsc/ingest.py b/pdsc/ingest.py index 711dacb..c39dd0e 100644 --- a/pdsc/ingest.py +++ b/pdsc/ingest.py @@ -13,6 +13,11 @@ TriSegmentedFootprint, SegmentTree) from .util import standard_progress_bar +# https://tharsis.gsfc.nasa.gov/geodesy.html +MARS_RADIUS_M = 3396200. +#https://nssdc.gsfc.nasa.gov/planetary/factsheet/moonfact.html +MOON_RADIUS_M = 1736000 + DEFAULT_CONFIG_DIR = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'config', @@ -134,7 +139,7 @@ def store_metadata(outputfile, instrument, table, config): for v in progress(cur.fetchall()) ] -def store_segments(outputfile, metadata, config): +def store_segments(outputfile, metadata, config, body_radius=MARS_RADIUS_M): """ Segments observations corresponding to each entry in ``metadata``, and stores these segments in a SQL database @@ -159,6 +164,8 @@ def store_segments(outputfile, metadata, config): - ``localizer_kwargs``: the ``kwargs`` that will be supplied to the :py:meth:`~pdsc.localization.get_localizer` function for determining observation footprints + :param body_radius: + radius of celestial body, default is mars :return: a list of :py:class:`~pdsc.segment.TriSegment` objects for segments across all observations @@ -172,11 +179,16 @@ def store_segments(outputfile, metadata, config): progress = standard_progress_bar('Segmenting footprints') for m in progress(metadata): try: - s = TriSegmentedFootprint(m, resolution, localizer_kwargs) + s = TriSegmentedFootprint(m, resolution, localizer_kwargs, body_radius) for si in s.segments: segments.append(si) - observation_ids.append(s.metadata.observation_id) - + # erd: check if observation_id attribute exists + if hasattr(s.metadata, 'observation_id'): + observation_ids.append(s.metadata.observation_id) + else: + raise ValueError('Please rename your index col to observation_id in the config file') + # erd: lroc does not have observation_id + #observation_ids.append(s.metadata.file_specification_name) except (TypeError, ValueError): continue @@ -208,7 +220,7 @@ def store_segments(outputfile, metadata, config): return segments -def store_segment_tree(outputfile, segments): +def store_segment_tree(outputfile, segments, body_radius=MARS_RADIUS_M): """ Constructs a ball tree index for segmented observations and saves the resulting data structure to the specified output file. @@ -218,8 +230,14 @@ def store_segment_tree(outputfile, segments): :param segments: a collection of :py:class:`~pdsc.segment.TriSegment` objects + + :body_radius: + celestial body radius, default is Mars """ - tree = SegmentTree(segments) + if body_radius == MARS_RADIUS_M: + tree = SegmentTree(segments) + else: + tree = SegmentTree(segments, True, body_radius) tree.save(outputfile) def ingest_idx(label_file, table_file, configpath, outputdir): @@ -241,6 +259,14 @@ def ingest_idx(label_file, table_file, configpath, outputdir): will be stored """ instrument, table = parse_table(label_file, table_file) + + if 'lroc' in instrument: + # use moon radius + body_radius = MOON_RADIUS_M + else: + # default radius is Mars + body_radius = MARS_RADIUS_M + if os.path.isdir(configpath): configfile = os.path.join(configpath, '%s_metadata.yaml' % instrument) else: @@ -266,10 +292,13 @@ def ingest_idx(label_file, table_file, configpath, outputdir): outputdir, '%s%s' % (instrument, SEGMENT_DB_SUFFIX) ) - segments = store_segments(outputfile, metadata, config) + + segments = store_segments(outputfile, metadata, config, body_radius) outputfile = os.path.join( outputdir, '%s%s' % (instrument, SEGMENT_TREE_SUFFIX) ) - store_segment_tree(outputfile, segments) + + store_segment_tree(outputfile, segments, body_radius) + diff --git a/pdsc/localization.py b/pdsc/localization.py index 614a2e6..5dc99a0 100644 --- a/pdsc/localization.py +++ b/pdsc/localization.py @@ -32,6 +32,11 @@ MARS_RADIUS_M = 3396200. MARS_FLATTENING = 1.0 / 169.8 +# erd: added moon info (before, assumed Mars only) +#https://nssdc.gsfc.nasa.gov/planetary/factsheet/moonfact.html +MOON_RADIUS_M = 1736000 +MOON_FLATTENING = 0.0012 + LOCALIZERS = {} register_localizer = registerer(LOCALIZERS) @@ -63,6 +68,10 @@ def geodesic_distance(latlon1, latlon2, radius=MARS_RADIUS_M): >>> geodesic_distance((0, 0), (0, np.pi)) 10669476.970121656 """ + #print('The moons radius is: 1736000') + #print('Radius used is: ') + #print(radius) + #print('------') haversine = DistanceMetric.get_metric('haversine') return float(radius*haversine.pairwise([latlon1], [latlon2])) @@ -198,6 +207,7 @@ def latlon_to_pixel(self, lat, lon, resolution_m=None, resolution_pix=0.1): loc = np.deg2rad([lat, lon]) + #print(self.BODY_RADIUS) def f(u): loc_u = np.deg2rad(self.pixel_to_latlon(*u)) return geodesic_distance(loc, loc_u, self.BODY_RADIUS) @@ -217,6 +227,7 @@ class GeodesicLocalizer(Localizer): """ BODY = Geodesic(MARS_RADIUS_M, MARS_FLATTENING) + """ A :py:class:`~geographiclib.geodesic.Geodesic` object describing the target body @@ -477,8 +488,9 @@ def __init__(self, proj_type, proj_latitude, proj_longitude, np.sqrt(a**2 + b**2) ) self.cos_proj_lat = np.cos(self.proj_latitude) - def _equirect_pixel_to_latlon(self, row, col): + # equations come from section 3.5.1 + # https://hirise.lpl.arizona.edu/pdf/HiRISE_RDR_v12_DTM_11_25_2009.pdf x = (col - self.col_offset)*self.map_scale y = -(row - self.row_offset)*self.map_scale return ( @@ -513,6 +525,8 @@ def _polar_pixel_to_latlon(self, row, col): return lat, lon def _polar_latlon_to_pixel(self, lat, lon): + # equations come from section 3.5.2 + # https://hirise.lpl.arizona.edu/pdf/HiRISE_RDR_v12_DTM_11_25_2009.pdf lat_rad = np.deg2rad(lat) lon_rad = np.deg2rad(lon % 360.) T = np.tan((np.pi / 4.0) - np.abs(lat_rad / 2.0)) @@ -726,6 +740,64 @@ def __init__(self, metadata): corners, 1.0, 1.0, 1 ) +class LrocCdrLocalizer(FourCornerLocalizer): + """ + A localizer for the LROC CDR observations (subclass of + :py:class:`FourCornerLocalizer`) + """ + + DEFAULT_RESOLUTION_M = 1/3*(1e-6) + """ + Sets the default resolution for lroc CDR localization + 1/3 of that of HiRISE + """ + + def __init__(self, metadata, downsamp): + """ + :param metadata: + "lroc_cdr" :py:class:`~pdsc.metadata.PdsMetadata` object + :param downsamp: + value of 1.0, no downsampling for LrocCdrLocalizer + """ + corners = np.array([ + [metadata.upper_left_latitude, metadata.upper_left_longitude], + [metadata.upper_right_latitude, metadata.upper_right_longitude], + [metadata.lower_right_latitude, metadata.lower_right_longitude], + [metadata.lower_left_latitude, metadata.lower_left_longitude], + ]) + super(LrocCdrLocalizer, self).__init__( + corners, metadata.lines/downsamp, metadata.samples/downsamp, 1 + ) + +class LrocCdrBrowseLocalizer(LrocCdrLocalizer): + """ + A localizer for the LROC CDR observations (subclass of + :py:class:`FourCornerLocalizer`) + """ + + DEFAULT_RESOLUTION_M = 1/3*(1e-6)*2 + """ + Sets the default resolution for lroc CDR localization + 1/3 of that of HiRISE, plus factor of 2 for browse + """ + + LROC_DOWNSAMP = 2.0 + """ + The default downsample amount for browse imagery + """ + + def __init__(self, metadata, downsamp): + """ + :param metadata: + "lroc_cdr" :py:class:`~pdsc.metadata.PdsMetadata` object + :param downsamp: + the downsample amount of lroc browse image (if it varies from the default + value) + """ + if downsamp < 1: + raise ValueError('Invalid downsample: %f' % downsamp) + super(LrocCdrBrowseLocalizer, self).__init__(metadata, downsamp) + class HiRiseRdrLocalizer(MapLocalizer): """ A localizer for the HiRISE RDR (map-projected) observations (subclass of @@ -817,6 +889,25 @@ def hirise_rdr_localizer(metadata, nomap=False, browse=False, else: return HiRiseRdrLocalizer(metadata) +@register_localizer('lroc_cdr') +def lroc_cdr_localizer(metadata, browse=False, downsamp=LrocCdrBrowseLocalizer.LROC_DOWNSAMP): + """ + Constructs the LROC CDR localizer (data is not map projected) + + :param metadata: + "lroc_cdr" :py:class:`~pdsc.metadata.PdsMetadata` object + :param browse: + construct localizer for the BROWSE data product + :param downsamp: + if ``browse=True``, use this value for the downsample amount + + :return: a :py:class:`Localizer` for the appropriate data product + """ + if browse: + return LrocCdrBrowseLocalizer(metadata, downsamp) + else: + return LrocCdrLocalizer(metadata, 1.0) + @register_localizer('moc') class MocLocalizer(GeodesicLocalizer): """ @@ -855,9 +946,9 @@ def get_localizer(metadata, *args, **kwargs): :param metadata: a :py:class:`~pdsc.metadata.PdsMetadata` object for an observation - :param \*args: + :param *args: additional args provided to the localizer constructor - :param \**kwargs: + :param **kwargs: additional kwargs provided to the localizer constructor :return: a :py:class:`Localizer` for the observation @@ -871,5 +962,4 @@ def get_localizer(metadata, *args, **kwargs): if metadata.instrument not in LOCALIZERS: raise IndexError( 'No localizer implemented for %s' % metadata.instrument) - return LOCALIZERS[metadata.instrument](metadata, *args, **kwargs) diff --git a/pdsc/segment.py b/pdsc/segment.py index 83b58d6..c0baf57 100644 --- a/pdsc/segment.py +++ b/pdsc/segment.py @@ -73,11 +73,12 @@ class SegmentTree(object): observation segments within some radius of a query point """ - def __init__(self, segments, verbose=True): + def __init__(self, segments, verbose=True, body_radius=MARS_RADIUS_M): """ :param segments: collection of all observation segments :param verbose: if ``True`` display a progress bar as the index is being built + :param body_radius: celestial body radius, default is Mars """ progress = standard_progress_bar('Finding segment centers', verbose) data = np.deg2rad([ @@ -92,6 +93,7 @@ def __init__(self, segments, verbose=True): if verbose: print('Building index...') self.ball_tree = BallTree(data, metric='haversine') + self.body_radius = body_radius if verbose: print('...done.') def query_point(self, point): @@ -104,7 +106,7 @@ def query_point(self, point): :return: a collection of segment ids for segments that satisfy the query """ total_radius = point.radius + self.max_radius - haversine_radius = total_radius / MARS_RADIUS_M + haversine_radius = total_radius / self.body_radius X = np.deg2rad(point.latlon).reshape((1, -1)) return self.ball_tree.query_radius(X, haversine_radius)[0] @@ -118,7 +120,7 @@ def query_segment(self, segment): :return: a collection of segment ids for segments that satisfy the query """ total_radius = segment.radius + self.max_radius - haversine_radius = total_radius / MARS_RADIUS_M + haversine_radius = total_radius / self.body_radius X = np.deg2rad([[segment.center_latitude, segment.center_longitude]]) return self.ball_tree.query_radius(X, haversine_radius)[0] @@ -149,7 +151,7 @@ class TriSegment(object): for indexing and efficient querying. """ - def __init__(self, latlon0, latlon1, latlon2): + def __init__(self, latlon0, latlon1, latlon2, body_radius=MARS_RADIUS_M): """ The three points of the triangular segment are enumerated in *counterclockwise* order looking down on the surface. Each point is a @@ -163,6 +165,7 @@ def __init__(self, latlon0, latlon1, latlon2): represents the third point (index 2) in a triangular segment """ self.latlon_points = np.array([latlon0, latlon1, latlon2]) + self.body_radius = body_radius self._center_longitude = None self._center_latitude = None self._xyz_points = None @@ -227,7 +230,7 @@ def radius(self): if self._radius is None: llcenter = np.deg2rad([self.center_latitude, self.center_longitude]) self._radius = np.max([ - geodesic_distance(llcenter, np.deg2rad(ll)) + geodesic_distance(llcenter, np.deg2rad(ll), radius=self.body_radius) for ll in self.latlon_points ]) return self._radius @@ -307,7 +310,7 @@ def distance_to_point(self, xyz): p = np.deg2rad(xyz2latlon(xyz)) return np.min([ - geodesic_distance(p, pi) + geodesic_distance(p, pi, radius=self.body_radius) for pi in np.deg2rad(points_to_check) ]) @@ -345,7 +348,7 @@ class SegmentedFootprint(with_metaclass(abc.ABCMeta, object)): Base class for segmenting an observation footprint """ - def __init__(self, metadata, resolution, localizer_kwargs): + def __init__(self, metadata, resolution, localizer_kwargs, body_radius=MARS_RADIUS_M): """ :param metadata: a :py:class:`~pdsc.metadata.PdsMetadata` object @@ -354,9 +357,12 @@ def __init__(self, metadata, resolution, localizer_kwargs): :param localizer_kwargs: the ``kwargs`` passed to the localizer used to convert observation pixel coordinates into real-world coordinates + :param body_radius: + celestial body radius """ self.metadata = metadata self.resolution = resolution + self.body_radius = body_radius self.localizer = get_localizer(metadata, **localizer_kwargs) n_row_chunks = int(np.ceil( self.localizer.observation_length_m / resolution @@ -398,8 +404,8 @@ def _segment(self): for c in range(L.shape[1]-1): for r in range(L.shape[0]-1): if self.localizer.flight_direction > 0: - yield TriSegment(L[r, c], L[r, c+1], L[r+1, c]) - yield TriSegment(L[r+1, c+1], L[r+1, c], L[r, c+1]) + yield TriSegment(L[r, c], L[r, c+1], L[r+1, c], body_radius=self.body_radius) + yield TriSegment(L[r+1, c+1], L[r+1, c], L[r, c+1], body_radius=self.body_radius) else: - yield TriSegment(L[r, c], L[r+1, c], L[r, c+1]) - yield TriSegment(L[r+1, c+1], L[r, c+1], L[r+1, c]) + yield TriSegment(L[r, c], L[r+1, c], L[r, c+1], body_radius=self.body_radius) + yield TriSegment(L[r+1, c+1], L[r, c+1], L[r+1, c], body_radius=self.body_radius) diff --git a/pdsc/table.py b/pdsc/table.py index d757e54..b789b74 100644 --- a/pdsc/table.py +++ b/pdsc/table.py @@ -68,6 +68,18 @@ def themis_datetime(s): """ return datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f') +def lroc_datetime(s): + """ + Parses date/time format found in lroc cumulative index files + + :param s: string + :return: :py:class:`datetime.datetime` object + + >>> lroc_datetime('1985-10-26 01:20:00') + datetime.datetime(1985, 10, 26, 1, 20) + """ + return datetime.strptime(s.strip(), '%Y-%m-%d %H:%M:%S.%f') + def hirise_datetime(s): """ Parses date/time format found in HiRISE cumulative index files @@ -105,6 +117,20 @@ def moc_observation_id(s): """ return s.replace('/', '') +@register_determiner('lroc_cdr') +def lroc_cdr_determiner(label_contents): + """ + Determines whether a cumulative index file is for LROC CDR products + :param label_contents: PDS cumulative index LBL file contents + :return: ``True`` iff this label file is for LROC CDR products + """ + + return ( + 'LROC' in label_contents and + 'CDR_CATALOG_INDEX' in label_contents + ) + + @register_determiner('hirise_edr') def hirise_edr_determiner(label_contents): """ @@ -385,7 +411,6 @@ def __init__(self, label_file, table_file): with open(label_file, 'r') as f: columns = self._parse(f) - if columns is None: raise RuntimeError('Error parsing table') @@ -485,7 +510,6 @@ def get_column(self, column_name_or_idx, progress=True, cache=True): f.seek(r*self.row_bytes + column.start_byte - 1) value = f.read(column.length) values.append(value) - try: data_column = np.array(values, dtype=column.dtype) except TypeError: @@ -610,6 +634,27 @@ class HiRiseTableColumn(PdsTableColumn): Defines special types for the HiRISE observation metadata """ +# **************************************************************************** +# LROC +# **************************************************************************** + +class LrocTableColumn(PdsTableColumn): + """ + A subclass of :py:class:`PdsTableColumn` for the LROC NAC instrument to define + some special types + Datetimes follow those of HiRISE + """ + + SPECIAL_TYPES = { + 'START_TIME': PdsColumnType(lroc_datetime), + 'STOP_TIME': PdsColumnType(lroc_datetime), + 'SPACECRAFT_CLOCK_START_COUNT': PdsColumnType(ctx_sclk), + 'SPACECRAFT_CLOCK_STOP_COUNT': PdsColumnType(ctx_sclk), + } + """ + Defines special types for the LROC observation metadata + """ + @register_table('hirise_edr') class HiRiseEdrTable(PdsTable): """ @@ -655,6 +700,28 @@ class HiRiseRdrTable(PdsTable): The HiRISE RDR table has a custom name """ +# **************************************************************************** +# LROC CDR +# **************************************************************************** + +@register_table('lroc_cdr') +class LrocCdrTable(PdsTable): + """ + A subclass of :py:class:`PdsTable` for the LROC NAC instrument that uses the + custom :py:class:`LrocTableColumn` class + """ + + COLUMN_CLASS = LrocTableColumn + """ + The :py:class:`LrocCdrTable` class should use + :py:class:`LrocTableColumn` for parsing columns + """ + + TABLE_OBJECT_NAME = 'INDEX_TABLE' + """ + The Lroc CDR table has a custom name + """ + # **************************************************************************** # MOC # **************************************************************************** @@ -709,5 +776,4 @@ def parse_table(label_file, table_file): if instrument not in INSTRUMENT_TABLES: raise ValueError('Table parsing not implemented for %s' % instrument) - return instrument, INSTRUMENT_TABLES[instrument](label_file, table_file) diff --git a/test/test_ingest.py b/test/test_ingest.py index 18259fa..5bb9df1 100644 --- a/test/test_ingest.py +++ b/test/test_ingest.py @@ -153,11 +153,11 @@ def test_ingest_idx(mock_parse_table, mock_store_segment_tree, ) mock_store_segments.assert_called_with( os.path.join('test_output', 'instrument_name_segments.db'), - 'metadata', 'config_contents' + 'metadata', 'config_contents', 3396200.0 ) mock_store_segment_tree.assert_called_with( os.path.join('test_output', 'instrument_name_segment_tree.pkl'), - 'segments' + 'segments', 3396200.0 ) @unit diff --git a/test/test_localization.py b/test/test_localization.py index edd1d4b..64012f4 100644 --- a/test/test_localization.py +++ b/test/test_localization.py @@ -7,7 +7,7 @@ from numpy.testing import assert_allclose from pdsc.localization import ( MapLocalizer, HiRiseRdrLocalizer, HiRiseRdrBrowseLocalizer, Localizer, - xyz2latlon, get_localizer, GeodesicLocalizer, MARS_RADIUS_M + xyz2latlon, get_localizer, GeodesicLocalizer, MARS_RADIUS_M, LrocCdrBrowseLocalizer ) from .cosmic_test_tools import unit @@ -335,6 +335,57 @@ ((-12.059007889850992, -69.11293214267153), (7168, 5056)), ((-12.78793820020455, -69.02157480900689), (0, 5056)), ] +# LROC Test cases are based on comparing the metadata with the +# localization for the 4 corners and center of the image + +LROC_NAC_M101013931LC_META = PdsMetadata( + 'lroc_cdr', center_latitude=-89.34, center_longitude=55.26, + lower_left_latitude=-88.58, lower_left_longitude=19.44, + upper_left_latitude=-89.1, upper_left_longitude=130.66, + upper_right_latitude=-89.15, upper_right_longitude=134.15, + lower_right_latitude=-88.6, lower_right_longitude=16.79, + lines=52224, samples=2532) + +# Test the 4 corners +LROC_NAC_M101013931LC_TEST_CASE = [ + ((-88.58, 19.44), (52224, 0)), # lower left + ((-89.1, 130.66), (0, 0)), # upper left + ((-89.15, 134.15), (0, 2532)), # upper right + ((-88.6, 16.79), (52224, 2532)), # lower right +] + + +LROC_NAC_M101014437RC_META = PdsMetadata( + 'lroc_cdr', center_latitude=-63.14, center_longitude=354.8, + lower_left_latitude=-63.09, lower_left_longitude=354.89, + upper_left_latitude=-63.18, upper_left_longitude=354.9, + upper_right_latitude=-63.18, upper_right_longitude=354.71, + lower_right_latitude=-63.09, lower_right_longitude=354.71, + lines=5120, samples=5064) + +# Test the 4 corners +LROC_NAC_M101014437RC_TEST_CASE = [ + ((-63.09, 354.89), (5120, 0)), # lower left + ((-63.18, 354.9), (0, 0)), # upper left + ((-63.18, 354.71), (0, 5064)), # upper right + ((-63.09, 354.71), (5120, 5064)), # lower right +] + +LROC_NAC_M191761375RC_META = PdsMetadata( + 'lroc_cdr', center_latitude=80.52, center_longitude=188.83, + lower_left_latitude=81.53, lower_left_longitude=190.57, + upper_left_latitude=79.48, upper_left_longitude=189.11, + upper_right_latitude=79.51, upper_right_longitude=187.41, + lower_right_latitude=81.56, lower_right_longitude=188.46, + lines=23552, samples=5064) + +# Test the 4 corners +LROC_NAC_M191761375RC_TEST_CASE = [ + ((81.53, 190.57), (23552, 0)), # lower left + ((79.48, 189.11), (0, 0)), # upper left + ((79.51, 187.41), (0, 5064)), # upper right + ((81.56, 188.46), (23552, 5064)), # lower right +] @unit @pytest.mark.parametrize( @@ -353,20 +404,28 @@ CTX_P06_003181_0946_XI_85S260W_TEST_CASE), (CTX_T01_000849_1676_XI_12S069W_META, CTX_T01_000849_1676_XI_12S069W_TEST_CASE), + (LROC_NAC_M101013931LC_META, LROC_NAC_M101013931LC_TEST_CASE), + (LROC_NAC_M101014437RC_META, LROC_NAC_M101014437RC_TEST_CASE), + (LROC_NAC_M191761375RC_META, LROC_NAC_M191761375RC_TEST_CASE) ] ) def test_localizer(metadata, latlons_pixels, browse=False): if metadata.instrument == 'hirise_rdr': localizer = get_localizer(metadata, browse=browse) + elif metadata.instrument == 'lroc_cdr': + localizer = get_localizer(metadata, browse=browse) else: localizer = get_localizer(metadata) for (lat, lon), (row, col) in latlons_pixels: if browse: - factor = ( - float(HiRiseRdrBrowseLocalizer.HIRISE_BROWSE_WIDTH) / - metadata.samples - ) + if metadata.instrument == 'hirise_rdr': + factor = ( + float(HiRiseRdrBrowseLocalizer.HIRISE_BROWSE_WIDTH) / + metadata.samples + ) + elif metadata.instrument == 'lroc_cdr': + factor = float(1.0/LrocCdrBrowseLocalizer.LROC_DOWNSAMP) row *= factor col *= factor @@ -407,6 +466,35 @@ def test_localizer(metadata, latlons_pixels, browse=False): # Run the same set of tests for the browse localizer... test_localizer(metadata, latlons_pixels, browse=True) + + if metadata.instrument == 'lroc_cdr': + if not browse: + # Run the same set of tests for the browse localizer + test_localizer(metadata, latlons_pixels, browse=True) + + # Test that the center lat/lon values are within a tolerance (100s meters) + # of the calculated center values. Glob is assumed to be a sphere so parallax causes these errors + # test with browse false + localizer = get_localizer(metadata, browse=False) + row_c, col_c = localizer.latlon_to_pixel(metadata.center_latitude, metadata.center_longitude) + assert_allclose((row_c, col_c), (metadata.lines // 2, metadata.samples // 2), atol=450) + # Test with browse=True, with default value + localizer = get_localizer(metadata, browse=True) + row_c, col_c = localizer.latlon_to_pixel(metadata.center_latitude, metadata.center_longitude) + assert_allclose((row_c, col_c), (metadata.lines // 4, metadata.samples // 4), atol=225) + # Test with passing in downsample amount + def test_lroc_downsamp(downsamp_value): + localizer = get_localizer(metadata, browse=True, downsamp=downsamp_value) + row_c, col_c = localizer.latlon_to_pixel(metadata.center_latitude, metadata.center_longitude) + assert_allclose((row_c, col_c), (metadata.lines // (2*downsamp_value), metadata.samples // (2*downsamp_value)), atol=(450/downsamp_value)) + for ii in [1.0, 2.0, 3.0, 4.0, 5.0]: + test_lroc_downsamp(ii) + # Check raises exception if pass in not allowed downsample amount + with pytest.raises(Exception): + localizer = get_localizer(metadata, browse=True, downsamp=0.5) + with pytest.raises(Exception): + localizer = get_localizer(metadata, browse=True, downsamp=-1) + # Regression tests for all CCDs/channels for HiRISE EDRs HIRISE_EDR_PSP_001334_2645_TEST_CASES = [ ( 8250, 256, 1.2696, 'BG12', 0, 4, 84.3370604, -16.0018754, 0, 0),