From feb58177ad92aef94a2e444ff6045fd930f49a31 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 10 Dec 2025 13:49:32 -0700 Subject: [PATCH 01/21] safety push --- oops/backplane/limb.py | 23 +++++++++++++++++++---- oops/hosts/galileo/ssi/__init__.py | 10 ++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/oops/backplane/limb.py b/oops/backplane/limb.py index c7817574..e2e4739e 100755 --- a/oops/backplane/limb.py +++ b/oops/backplane/limb.py @@ -30,8 +30,9 @@ def limb_altitude(self, event_key, zmin=None, zmax=None, scaled=False): radius = body.surface.radii.max() if zmin is not None: zmin = zmin * radius - if zmin is not None: - zmax = zmax * radius + if zmax is not None: + if zmin is not None: + zmax = zmax * radius key = ('limb_altitude', event_key, zmin, zmax) if key in self.backplanes: @@ -159,7 +160,7 @@ def limb_latitude(self, event_key, lat_type='centric'): return self.register_backplane(key, latitude) #=============================================================================== -def limb_clock_angle(self, event_key): +def limb_clock_angle(self, event_key, zmin=None, zmax=None, scaled=False): """Angular location around the limb, measured clockwise from the projected north pole. @@ -168,8 +169,16 @@ def limb_clock_angle(self, event_key): limb_altitude backplane key, in which case this backplane inherits the mask of the given backplane array. + zmin lower limit on altitude; lower values are masked. + zmax upper limit on altitude. + scaled if True, zmin and zmax are in units of the maximum body radius. """ + # Get the altitude backplane + altitude_key = ('limb_altitude', event_key, zmin, zmax) + altitude = limb_altitude(self, event_key, zmin=zmin, zmax=zmax, scaled=scaled) + + # Create the clock angle backplane (event_key, backplane_key) = self._event_and_backplane_keys(event_key, LIMB_BACKPLANES, default='LIMB') @@ -194,8 +203,14 @@ def limb_clock_angle(self, event_key): event = polar_surface.apply_coords_to_event(event, obs=self.obs_event, axes=2, derivs=self.ALL_DERIVS) + clock_angle = event.coord2 + + ### use self._remasked_backplane + # copy altitude mask + if np.any(altitude.mask): + clock_angle = clock_angle.remask_or(altitude.mask) - return self.register_backplane(key, event.coord2) + return self.register_backplane(key, clock_angle) ################################################################################ diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index dbd04628..2e3a7680 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -42,7 +42,8 @@ def from_file(filespec, filespec = FCPath(filespec) # Load the PDS label - label = pds3.get_label(filespec) +# label = pds3.get_label(filespec) + label = pdsparser.PdsLabel.from_file(filespec).as_dict() # Load the data array local_path = filespec.retrieve() @@ -211,6 +212,8 @@ def __init__(self, meta_dict): self.target = meta_dict['TARGET_NAME'] # Telemetry mode + if not 'TELEMETRY_FORMAT_ID' in meta_dict: + meta_dict['TELEMETRY_FORMAT_ID'] = 'NONE' self.mode = meta_dict['TELEMETRY_FORMAT_ID'] # Window @@ -366,9 +369,12 @@ def initialize(planets=None, asof=None, SSI.fovs['HCM'] = fov_full # Inference based on inspection # hmmm, actually C0248807700R.img is 800x200 # maybe this is just a cropped full fov + SSI.fovs['NONE'] = fov_full # Inference based on inspection # Construct the SpiceFrame - _ = oops.frame.SpiceFrame("GLL_SCAN_PLATFORM") + # SSI images are spaced as closely as 1 unit in the file name, which + # corresponds to 80 clock ticks. Therefore, we use a tolerance of +/-40 + _ = oops.frame.SpiceType1Frame("GLL_SCAN_PLATFORM", -77, 40) # Load kernels Galileo.load_kernels() From 0f6090b5bad2276fa9a94b9801bb5564b4e34f0c Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Thu, 11 Dec 2025 09:04:51 -0700 Subject: [PATCH 02/21] coderabbit fixes --- oops/backplane/limb.py | 4 +--- oops/hosts/galileo/ssi/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/oops/backplane/limb.py b/oops/backplane/limb.py index e2e4739e..a04d61b0 100755 --- a/oops/backplane/limb.py +++ b/oops/backplane/limb.py @@ -31,8 +31,7 @@ def limb_altitude(self, event_key, zmin=None, zmax=None, scaled=False): if zmin is not None: zmin = zmin * radius if zmax is not None: - if zmin is not None: - zmax = zmax * radius + zmax = zmax * radius key = ('limb_altitude', event_key, zmin, zmax) if key in self.backplanes: @@ -205,7 +204,6 @@ def limb_clock_angle(self, event_key, zmin=None, zmax=None, scaled=False): derivs=self.ALL_DERIVS) clock_angle = event.coord2 - ### use self._remasked_backplane # copy altitude mask if np.any(altitude.mask): clock_angle = clock_angle.remask_or(altitude.mask) diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index 2e3a7680..1b2452da 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -11,7 +11,6 @@ import pdsparser import oops -from oops.hosts import pds3 from oops.hosts.galileo import Galileo from filecache import FCPath @@ -42,7 +41,6 @@ def from_file(filespec, filespec = FCPath(filespec) # Load the PDS label -# label = pds3.get_label(filespec) label = pdsparser.PdsLabel.from_file(filespec).as_dict() # Load the data array @@ -212,7 +210,7 @@ def __init__(self, meta_dict): self.target = meta_dict['TARGET_NAME'] # Telemetry mode - if not 'TELEMETRY_FORMAT_ID' in meta_dict: + if 'TELEMETRY_FORMAT_ID' not in meta_dict: meta_dict['TELEMETRY_FORMAT_ID'] = 'NONE' self.mode = meta_dict['TELEMETRY_FORMAT_ID'] From 2ae7145bfeabaa5a28ad38ebdf07c7d43565fff0 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Tue, 3 Feb 2026 08:24:16 -0700 Subject: [PATCH 03/21] coderabbit --- oops/backplane/limb.py | 13 +------------ oops/hosts/galileo/ssi/__init__.py | 5 ++--- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/oops/backplane/limb.py b/oops/backplane/limb.py index a04d61b0..b1ba06f0 100755 --- a/oops/backplane/limb.py +++ b/oops/backplane/limb.py @@ -159,7 +159,7 @@ def limb_latitude(self, event_key, lat_type='centric'): return self.register_backplane(key, latitude) #=============================================================================== -def limb_clock_angle(self, event_key, zmin=None, zmax=None, scaled=False): +def limb_clock_angle(self, event_key): """Angular location around the limb, measured clockwise from the projected north pole. @@ -168,15 +168,8 @@ def limb_clock_angle(self, event_key, zmin=None, zmax=None, scaled=False): limb_altitude backplane key, in which case this backplane inherits the mask of the given backplane array. - zmin lower limit on altitude; lower values are masked. - zmax upper limit on altitude. - scaled if True, zmin and zmax are in units of the maximum body radius. """ - # Get the altitude backplane - altitude_key = ('limb_altitude', event_key, zmin, zmax) - altitude = limb_altitude(self, event_key, zmin=zmin, zmax=zmax, scaled=scaled) - # Create the clock angle backplane (event_key, backplane_key) = self._event_and_backplane_keys(event_key, LIMB_BACKPLANES, @@ -204,10 +197,6 @@ def limb_clock_angle(self, event_key, zmin=None, zmax=None, scaled=False): derivs=self.ALL_DERIVS) clock_angle = event.coord2 - # copy altitude mask - if np.any(altitude.mask): - clock_angle = clock_angle.remask_or(altitude.mask) - return self.register_backplane(key, clock_angle) ################################################################################ diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index 3a9ca054..c96676fc 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -41,11 +41,10 @@ def from_file(filespec, filespec = FCPath(filespec) # Load the PDS label - label = pdsparser.PdsLabel.from_file(filespec).as_dict() + label = pdsparser.Pds3Label(filespec).as_dict() # Load the data array - local_path = filespec.retrieve() - vic = vicar.VicarImage.from_file(local_path) + vic = vicar.VicarImage.from_file(filespec) vicar_dict = vic.as_dict() # Get image metadata From 782474a6f74dd00b22a25bb81d1fffa57cce95c4 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Mon, 9 Feb 2026 10:20:18 -0700 Subject: [PATCH 04/21] remove superfluous _fill_limb_intercepts code block in limb_clock_angle --- oops/backplane/limb.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/oops/backplane/limb.py b/oops/backplane/limb.py index b1ba06f0..08ba236e 100755 --- a/oops/backplane/limb.py +++ b/oops/backplane/limb.py @@ -78,7 +78,7 @@ def _fill_limb_intercepts(self, event_key): 'squashed'), event.coord1) self.register_backplane(('latitude', event_key, 'squashed'), event.coord2) self.register_backplane(('limb_altitude', event_key, None, None), - event.coord3) + event.coord3) #=============================================================================== def limb_longitude(self, event_key, reference='iau', direction='west', @@ -183,11 +183,6 @@ def limb_clock_angle(self, event_key): if key in self.backplanes: return self.get_backplane(key) - # Make sure the limb event is defined - default_key = ('limb_clock_angle', event_key) - if default_key not in self.backplanes: - self._fill_limb_intercepts(event_key) - surface = Backplane.get_surface(event_key[1]) event = self.get_surface_event(event_key) From b57b8a733c326d182d58a262c4667fe063fffde7 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 11 Feb 2026 10:57:35 -0700 Subject: [PATCH 05/21] - fix missing import in tests/hosts/hst/__init__.py - update uvis.py, vims.py, and galileo/ssi/_init__.py to use pdsparser.Pds3Label() - move pds3.py to ideas/ --- {oops/hosts => ideas}/pds3.py | 0 oops/hosts/cassini/uvis.py | 3 +-- oops/hosts/cassini/vims.py | 15 +++++++-------- oops/hosts/galileo/ssi/__init__.py | 3 +-- tests/hosts/hst/__init__.py | 1 + 5 files changed, 10 insertions(+), 12 deletions(-) rename {oops/hosts => ideas}/pds3.py (100%) diff --git a/oops/hosts/pds3.py b/ideas/pds3.py similarity index 100% rename from oops/hosts/pds3.py rename to ideas/pds3.py diff --git a/oops/hosts/cassini/uvis.py b/oops/hosts/cassini/uvis.py index ee75b1ce..3a717959 100755 --- a/oops/hosts/cassini/uvis.py +++ b/oops/hosts/cassini/uvis.py @@ -11,7 +11,6 @@ import pdsparser from oops.hosts.cassini import Cassini -from oops.hosts import pds3 from filecache import FCPath @@ -41,7 +40,7 @@ def from_file(filespec, data=True, enclose=False, **parameters): # initialize() is called explicitly. # Get the label dictionary and data array dimensions - label = pds3.fast_dict(filespec) + label = pdsparser.Pds3Label(filespec, method='fast').as_dict() # Load any needed SPICE kernels tstart = julian.tdb_from_tai(julian.tai_from_iso(label['START_TIME'])) diff --git a/oops/hosts/cassini/vims.py b/oops/hosts/cassini/vims.py index 8b2c434a..0ee64460 100755 --- a/oops/hosts/cassini/vims.py +++ b/oops/hosts/cassini/vims.py @@ -18,7 +18,6 @@ import oops from oops.hosts.cassini import Cassini -from oops.hosts.pds3 import pds3 from filecache import FCPath @@ -79,7 +78,7 @@ # FMT files have fixed values ########################################################################################## -CORE_DESCRIPTION_FMT = pds3.fast_dict("""\ +CORE_DESCRIPTION_FMT = pdsparser.Pds3Label("""\ CORE_ITEM_BYTES = 2 CORE_ITEM_TYPE = SUN_INTEGER CORE_BASE = 0.0 @@ -93,9 +92,9 @@ CORE_MINIMUM_DN = -122 CORE_NAME = "RAW DATA NUMBER" CORE_UNIT = DIMENSIONLESS -""") +""", method='fast').as_dict() -SUFFIX_DESCRIPTION_FMT = pds3.fast_dict("""\ +SUFFIX_DESCRIPTION_FMT = pdsparser.Pds3Label("""\ GROUP = SAMPLE_SUFFIX SUFFIX_NAME = BACKGROUND SUFFIX_UNIT = DIMENSIONLESS @@ -130,9 +129,9 @@ SUFFIX_HIGH_INSTR_SAT = (-32765,-32765,-32765,-32765) SUFFIX_HIGH_REPR_SAT = (-32764,-32764,-32764,-32764) END_GROUP = BAND_SUFFIX -""") +""", method='fast').as_dict() -BAND_BIN_CENTER_FMT = pds3.fast_dict("""\ +BAND_BIN_CENTER_FMT = pdsparser.Pds3Label("""\ GROUP = BAND_BIN BAND_BIN_CENTER = (0.35,0.36,0.37,0.37,0.38,0.39,0.40,0.40,0.41,0.42, 0.42,0.43,0.44,0.45,0.45,0.46,0.47,0.48,0.49,0.49,0.50,0.51,0.51,0.52, @@ -184,7 +183,7 @@ 327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344, 345,346,347,348,349,350,351) END_GROUP = BAND_BIN -""") +""", method='fast').as_dict() ########################################################################################## # Standard class methods @@ -213,7 +212,7 @@ def from_file(filespec, data=True): filespec = FCPath(filespec) - label = pds3.fast_dict(filespec) + label = pdsparser.Pds3Label(filespec, method='fast').as_dict() # Insert "data_file" and "header_recs" # Convert ISIS .qub info to a standard PDS3 label dictionary diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index f1b34dac..feeaa5f0 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -11,7 +11,6 @@ import pdsparser import oops -from oops.hosts import pds3 from oops.hosts.galileo import Galileo from filecache import FCPath @@ -42,7 +41,7 @@ def from_file(filespec, filespec = FCPath(filespec) # Load the PDS label - label = pds3.get_label(filespec) + label = pdsparser.Pds3Label(filespec).as_dict() # Load the data array local_path = filespec.retrieve() diff --git a/tests/hosts/hst/__init__.py b/tests/hosts/hst/__init__.py index 20e5590d..385ca204 100755 --- a/tests/hosts/hst/__init__.py +++ b/tests/hosts/hst/__init__.py @@ -1,6 +1,7 @@ ################################################################################ # tests/hosts/hst/__init__.py ################################################################################ +import unittest class Test_HST(unittest.TestCase): From 4bca3b1323def461feb9df3f0024e9128049aa73 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 11 Feb 2026 12:03:31 -0700 Subject: [PATCH 06/21] coderabbit --- oops/hosts/cassini/uvis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oops/hosts/cassini/uvis.py b/oops/hosts/cassini/uvis.py index 3a717959..a26c5c0e 100755 --- a/oops/hosts/cassini/uvis.py +++ b/oops/hosts/cassini/uvis.py @@ -153,7 +153,7 @@ def get_qube(filespec, tstart, label, data, enclose): # Separate windows else: obslist = [] - for w in len(line0): + for w in range(len(line0)): obs = get_one_qube(label, detector, resolution, fov, cadence, frame_id, shape, array, samples, From ef5e9aaaf709541b91af67d53ba9d51052de38cd Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Fri, 20 Feb 2026 10:00:14 -0700 Subject: [PATCH 07/21] Use 'fast' method for reading label in galileo ssi. --- oops/hosts/galileo/ssi/__init__.py | 2 +- tests/hosts/juno/__init__.py | 470 +-------------------------- tests/hosts/juno/junocam/__init__.py | 5 +- 3 files changed, 5 insertions(+), 472 deletions(-) diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index c96676fc..070e93ed 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -41,7 +41,7 @@ def from_file(filespec, filespec = FCPath(filespec) # Load the PDS label - label = pdsparser.Pds3Label(filespec).as_dict() + label = pdsparser.Pds3Label(filespec, method='fast').as_dict() # Load the data array vic = vicar.VicarImage.from_file(filespec) diff --git a/tests/hosts/juno/__init__.py b/tests/hosts/juno/__init__.py index 17376c12..9d6a7ff3 100644 --- a/tests/hosts/juno/__init__.py +++ b/tests/hosts/juno/__init__.py @@ -1,471 +1,3 @@ ################################################################################ -# oops/inst/juno/__init__.py -# -# Utility functions for managing SPICE kernels while working with juno data -# sets. +# tests/hosts/juno/__init__.py ################################################################################ - -import numpy as np - -import julian -import spicedb -import cspyce -import oops - -from oops.body import Body - -################################################################################ -# Routines for managing the loading of C and SP kernels -################################################################################ - -# Make sure the leap seconds have been loaded -oops.spice.load_leap_seconds() - -# We load CK and SPK files on a very rough month-by-month basis. This is simpler -# than a more granular approach involving detailed calendar calculations. We -# divide the period October 1, 1997 to October 1, 2017 up into 240 "months" of -# equal length. Given any TDB, we quickly determine the month within which it -# falls. Each month is associated with a list of kernels that should be loaded -# whenever information is needed about any time within that month +/- 12 hours. -# The kernels needed for a given month only get loaded when they are needed, and -# are only loaded once. For any geometry calculation involving Juno, a quick -# call to load_ck(time) or load_spk(time) will ensure that the information is -# available. - -################################################################################ - -#******************************************************************************* -class Juno(object): - """An instance-free class to hold Juno-specific parameters.""" - - START_TIME = '2011-08-01' - STOP_TIME = '2025-08-01' - MONTHS = 168 # 14 years * 12 months/year - - TDB0 = julian.tdb_from_tai(julian.tai_from_iso(START_TIME)) - TDB1 = julian.tdb_from_tai(julian.tai_from_iso(STOP_TIME)) - DTDB = (TDB1 - TDB0) / MONTHS - SLOP = 43200. - - CK_LOADED = np.zeros(MONTHS, dtype='bool') # True if month was loaded - CK_LIST = np.empty(MONTHS, dtype='object') # Kernels needed by month - CK_DICT = {} # Dictionary keyed by filespec returns kernel info - # object, but only if loaded. - - SPK_LOADED = np.zeros(MONTHS, dtype='bool') - SPK_LIST = np.empty(MONTHS, dtype='object') - SPK_DICT = {} - - loaded_instruments = [] - - initialized = False - - #=========================================================================== - @staticmethod - def initialize(ck='reconstructed', planets=None, asof=None, - spk='reconstructed', gapfill=True, - mst_pck=True, irregulars=True): - """Intialize the Juno mission internals. - - After the first call, later calls to this function are ignored. - - Input: - ck,spk Used to specify which C and SPK kernels are used.: - 'reconstructed' for the reconstructed kernels (default); - 'predicted' for the predicted kernels; - 'none' to allow manual control of the C kernels. - planets A list of planets to pass to define_solar_system. None - or 0 means all. - asof Only use SPICE kernels that existed before this date; - None to ignore. - gapfill True to include gapfill CKs. False otherwise. - mst_pck True to include MST PCKs, which update the rotation - models for some of the small moons. - irregulars True to include the irregular satellites; - False otherwise. - """ - if Juno.initialized: return - - (ck, spk) = ('NONE', 'NONE') - - # Define some important paths and frames - Body.define_solar_system(Juno.START_TIME, Juno.STOP_TIME, - asof=asof, - planets=planets, - mst_pck=mst_pck, - irregulars=irregulars) - - ignore = oops.path.SpicePath('JUNO', 'JUPITER') - - spicedb.open_db() - - spk = spk.upper() - if spk == 'NONE': - - # This means no SPK will ever be loaded; handling is manual - Juno.initialize_kernels([], Juno.SPK_LIST) - Juno.SPK_LOADED = np.ones(Juno.MONTHS, dtype='bool') - else: - kernels = spicedb.select_spk(-61, name='JUNO_-SPK-' + spk, - time=(Juno.START_TIME, - Juno.STOP_TIME), - asof=asof) - Juno.initialize_kernels(kernels, Juno.SPK_LIST) - - ck = ck.upper() - if ck == 'NONE': - - # This means no CK will ever be loaded; handling is manual - Juno.initialize_kernels([], Juno.CK_LIST) - Juno.CK_LOADED = np.ones(Juno.MONTHS, dtype='bool') - else: - kernels = spicedb.select_ck(-61, name='JUNO_-CK-' + ck, - time=(Juno.START_TIME, - Juno.STOP_TIME), - asof=asof) - Juno.initialize_kernels(kernels, Juno.CK_LIST) - - # Load extra kernels if necessary - if gapfill and ck not in ('PREDICTED', 'NONE'): - _ = spicedb.furnish_ck(-61, name='JUNO_-CK-GAPFILL') - - spicedb.close_db() - - Juno.initialized = True - - #=========================================================================== - @staticmethod - def reset(): - """Reset the internal parameters. - - Can be useful for debugging. - """ - Juno.loaded_instruments = [] - - Juno.CK_LOADED = np.zeros(Juno.MONTHS, dtype='bool') - Juno.CK_LIST = np.empty(Juno.MONTHS, dtype='object') - Juno.CK_DICT = {} - - Juno.SPK_LOADED = np.zeros(Juno.MONTHS, dtype='bool') - Juno.SPK_LIST = np.empty(Juno.MONTHS, dtype='object') - Juno.SPK_DICT = {} - - Juno.initialized = False - - #=========================================================================== - @staticmethod - def load_ck(t): - """Ensure that the C kernels applicable at or near the given time have - been furnished. - - The time can be tai or tdb. - """ - Juno.load_kernels(t, t, Juno.CK_LOADED, Juno.CK_LIST, - Juno.CK_DICT) - - #=========================================================================== - @staticmethod - def load_cks(t0, t1): - """Ensure that all the C kernels applicable near or within the time - interval tdb0 to tdb1 have been furnished. - - The time can be tai or tdb. - """ - Juno.load_kernels(t0, t1, Juno.CK_LOADED, Juno.CK_LIST, - Juno.CK_DICT) - - #=========================================================================== - @staticmethod - def load_spk(t): - """Ensure that the SPK kernels applicable at or near the given time have - been furnished. - - The time can be tai or tdb. - """ - Juno.load_kernels(t, t, Juno.SPK_LOADED, Juno.SPK_LIST, - Juno.SPK_DICT) - - #=========================================================================== - @staticmethod - def load_spks(t0, t1): - """Ensure that all the SPK kernels applicable near or within the time - interval tdb0 to tdb1 have been furnished. - - The time can be tai or tdb. - """ - Juno.load_kernels(t0, t1, Juno.SPK_LOADED, Juno.SPK_LIST, - Juno.SPK_DICT) - - #=========================================================================== - @staticmethod - def load_kernels(t0, t1, loaded, lists, kernel_dict): - """Load kernal pool.""" - - from spicedb import get_spice_filecache_prefix - - SPICE_FILECACHE_PFX = get_spice_filecache_prefix() - - paths = SPICE_FILECACHE_PFX.retrieve([ - 'Juno/CK/juno_sc_rec_131006_131012_v01.bc', - 'Juno/SPK/spk_rec_131005_131014_131101.bsp', - 'Juno/CK/juno_sc_rec_161211_161217_v01.bc', - 'Juno/SPK/juno_rec_161115_170106_170113.bsp', - 'Juno/CK/juno_sc_rec_170702_170708_v01.bc', - 'Juno/SPK/juno_rec_170608_170728_170803.bsp', - 'Juno/CK/juno_sc_rec_171023_171025_v01.bc', - 'Juno/SPK/juno_rec_170918_171121_171127.bsp', - 'Juno/CK/juno_sc_rec_171215_171217_v01.bc', - 'Juno/SPK/juno_rec_171121_180113_180117.bsp', - 'Juno/CK/juno_sc_rec_180523_180524_v01.bc', - 'Juno/SPK/juno_rec_180429_180621_180626.bsp', - 'Juno/CK/juno_sc_rec_180906_180907_v01.bc', - 'Juno/SPK/juno_rec_180812_181004_181011.bsp', - 'Juno/CK/juno_sc_rec_190405_190406_v01.bc', - 'Juno/SPK/juno_rec_190312_190504_190509.bsp', - 'Juno/CK/juno_sc_rec_190911_190912_v01.bc', - 'Juno/SPK/juno_rec_190817_191010_191022.bsp', - 'Juno/CK/juno_sc_rec_200405_200411_v01.bc', - 'Juno/SPK/juno_rec_200316_200508_200512.bsp', - 'Juno/CK/juno_sc_rec_200719_200725_v01.bc', - 'Juno/SPK/juno_rec_200629_200822_200826.bsp', - 'Juno/CK/juno_sc_rec_201108_201114_v01.bc', - 'Juno/SPK/juno_rec_201014_201205_201208.bsp', - 'Juno/CK/juno_sc_rec_201227_210102_v01.bc', - 'Juno/SPK/juno_rec_201205_210127_210210.bsp', - 'Juno/CK/juno_sc_rec_210221_210227_v01.bc', - 'Juno/SPK/juno_rec_210127_210321_210329.bsp', - 'Juno/CK/juno_sc_rec_210221_210227_v01.bc', - 'Juno/SPK/juno_rec_210127_210321_210329.bsp', - 'Juno/CK/juno_sc_rec_190528_190529_v01.bc', - 'Juno/SPK/juno_rec_190504_190626_190627.bsp', - - 'Juno/CK/juno_sc_rec_160710_160716_v01.bc', - 'Juno/CK/juno_sc_rec_160717_160723_v01.bc', - 'Juno/SPK/spk_rec_160522_160729_160909.bsp', - - 'Juno/CK/juno_sc_rec_170827_170902_v01.bc', - 'Juno/CK/juno_sc_rec_170903_170909_v01.bc', - 'Juno/SPK/spk_rec_170728_170918_170922.bsp', - - 'Juno/CK/juno_sc_rec_180715_180716_v01.bc', - 'Juno/SPK/spk_rec_180620_180812_180821.bsp', - - 'General/LSK/naif0012.tls', - 'Juno/SCLK/jno_sclkscet_00128.tsc', - 'Juno/FK/juno_v12.tf', - 'Juno/IK/juno_junocam_v03.ti', - 'Juno/IK/juno_jiram_v02.ti', - 'Juno/SPK/de421.bsp', - 'Juno/SPK/de432s.bsp', - ]) - for path in paths: - cspyce.furnsh(path) - -### This would be best handled by creating a text file that contains a list of the -### file names to be loaded (starting with "ck/, "spk/" etc.). Maintain this file -### separately from the Python code. The Python code reads the list and furnishes -### inside a big loop. When a new SPK or CK is released, we just add it to the list -### and update it in the repo. See hosts/solar for how to allow a Python module to -### locate a file within its own directory path. The kernels can live inside the -### OOPS-Resources/SPICE tree, so you can determine the value of "kdir" from the -### value of (unittester_support.OOPS_RESOURCES_ + 'SPICE/Juno/'). -### -### Even better, adopt the Cassini module's method of just loading the CKs and SPKs -### as they are requested, because there are so many of them. It'll just furnish the -### CKs and SPKs when it needs them, in the background, silently. -### -### Other notes... -### -### There are a lot of duplicated files on Dropbox between -### OOPS-Resources/SPICE/Juno and test_data/juno/kernels. Better to keep a single -### set in the former. If kernels start to change out from underneath us in a way -### that breaks unit tests, that would be the time to start keeping a specific -### subset inside test_data/juno. -### -### The set of CKs and SPKs on Dropbox is currently incomplete. We need to start -### maintaining it, and continue to do so once we are generating metadata tables. -### -### I see on the NAIF ftp server that there are multiple versions of the CKs and -### SPKs, as there were for Cassini. We probably never need to worry about anything -### but the reconstructed. However, in order to allow for multiple future versions, -### I suggest you rename SPICE/Juno/CK to SPICE/Juno/CK-reconstructed and -### SPICE/Juno/SPK to SPICE/Juno/SPK-reconstructed. This will make for simpler file -### management going forward. -### -### The other day, you asked me to add a few kernels to SPICE.db. It's not clear to -### me why that was necessary. The kernel management method I described above should -### be sufficient for all Juno kernel management, and is the reason I've concluded -### we should get rid of the sqlite database. - - return - -## TODO: - # Find the range of months needed - m1 = int((t0 - Juno.TDB0) // Juno.DTDB) - m2 = int((t1 - Juno.TDB0) // Juno.DTDB) + 1 - - m1 = max(m1, 0) # ignore time limits outside mission duration - m2 = min(m2, Juno.MONTHS - 1) - - # Load any months not already loaded - for m in range(m1, m2+1): - if not loaded[m]: - for kernel in lists[m]: - filespec = kernel.filespec - if filespec not in kernel_dict: - spicedb.furnish_kernels([kernel]) - kernel_dict[filespec] = kernel - loaded[m] = True - - ######################################## - # Initialize the kernel lists - ######################################## - - #=========================================================================== - @staticmethod - def initialize_kernels(kernels, lists): - """After initialization, lists[m] is a the KernelInfo objects needed - within the specified month. - """ - for i in range(Juno.MONTHS): - lists[i] = [] - - for kernel in kernels: - - # Find the range of months applicable, extended by 12 hours - t0 = cspyce.str2et(kernel.start_time) - Juno.SLOP - t1 = cspyce.str2et(kernel.stop_time) + Juno.SLOP - - m1 = int((t0 - Juno.TDB0) // Juno.DTDB) - m2 = int((t1 - Juno.TDB0) // Juno.DTDB) + 1 - - m1 = max(m1, 0) # ignore time limits outside mission duration - m2 = min(m2, Juno.MONTHS - 1) - - # Add this kernel to each month's list - for m in range(m1, m2+1): - lists[m] += [kernel] - - ############################################################################ - # Routines for managing the loading other kernels - ############################################################################ - - #=========================================================================== - @staticmethod - def load_instruments(instruments=[], asof=None): - """Load the SPICE kernels and defines the basic paths and frames for - the Juno mission. - - It is generally only be called once. - - Input: - instruments an optional list of instrument names for which to load - frames kernels. The frames for JUNOCAM are always loaded. - - asof if this specifies a date or date-time in ISO format, - then only kernels that existed before the specified date - are used. Otherwise, the most recent versions are always - loaded. - """ - - # Load the default instruments on the first pass - if Juno.loaded_instruments == []: - instruments += ['JUNOCAM'] - - # On later calls, return quickly if there's nothing to do - if instruments == []: return - - # Check the formatting of the "as of" date - if asof is not None: - (day, sec) = julian.day_sec_from_iso(asof) - asof = julian.ymdhms_format_from_day_sec(day, sec) - - # Furnish instruments and frames - spicedb.open_db() - _ = spicedb.furnish_inst(-61, inst=instruments, asof=asof) - spicedb.close_db() - - ############################################################################ - # Routines for managing text kernel information - ############################################################################ - -### TODO: finish these routines... - - #=========================================================================== - @staticmethod - def spice_instrument_kernel(inst, asof=None): - """Return a dictionary containing the Instrument Kernel information. - - Also furnishes it for use by the SPICE tools. - - Input: - inst one of "JUNOCAM", etc. - asof an optional date in the past, in ISO date or date-time - format. If provided, then the information provided will - be applicable as of that date. Otherwise, the most - recent information is always provided. - - Return: a tuple containing: - the dictionary generated by textkernel.from_file() - the name of the kernel. - """ - if asof is not None: - (day,sec) = julian.day_sec_from_iso(stop_time) - asof = julian.ymdhms_format_from_day_sec(day, sec) - - spicedb.open_db() - kernel_info = spicedb.select_inst(-61, types='IK', inst='JUNOCAM', asof=asof) - spicedb.furnish_kernels(kernel_info, fast=True) - spicedb.close_db() - - return (spicedb.as_dict(kernel_info), spicedb.as_names(kernel_info)[0]) - - #=========================================================================== - @staticmethod - def spice_frames_kernel(asof=None): - """Return a dictionary containing the Juno Frames Kernel information. - - Also furnishes the kernels for use by the SPICE tools. - - Input: - asof an optional date in the past, in ISO date or date-time - format. If provided, then the information provided will - be applicable as of that date. Otherwise, the most - recent information is always provided. - - Return: a tuple containing: - the dictionary generated by textkernel.from_file() - an ordered list of the names of the kernels - """ - if asof is not None: - (day,sec) = julian.day_sec_from_iso(stop_time) - asof = julian.ymdhms_format_from_day_sec(day, sec) - - spicedb.open_db() - kernel_list = spicedb.select_inst(-61, types='FK', asof=asof) - spicedb.furnish_kernels(kernel_info, fast=True) - spicedb.close_db() - - return (spicedb.as_dict(kernel_list), spicedb.as_names(kernel_list)) - - #=========================================================================== - @staticmethod - def used_kernels(time, inst, return_all_planets=False): - """Return the list of kernels associated with a Juno observation at - a selected range of times. - """ - if return_all_planets: - bodies = [1, 199, 2, 299, 3, 399, 4, 499, 5, 599, 6, 699, - 7, 799, 8, 899] - if time[0] >= TOUR: - bodies += body.SATURN_MOONS_LOADED - else: - bodies += body.JUPITER_MOONS_LOADED - else: - if time[0] >= TOUR: - bodies = [6, 699] + body.SATURN_MOONS_LOADED - else: - bodies = [5, 599] + body.JUPITER_MOONS_LOADED - - return spicedb.used_basenames(time=time, inst=inst, sc=-61, - bodies=bodies) diff --git a/tests/hosts/juno/junocam/__init__.py b/tests/hosts/juno/junocam/__init__.py index 36af9346..b3ddf00c 100644 --- a/tests/hosts/juno/junocam/__init__.py +++ b/tests/hosts/juno/junocam/__init__.py @@ -1,5 +1,5 @@ ################################################################################ -# oops/inst/juno/junocam.py +# oops/inst/juno/junocam/__init__.py ################################################################################ import os @@ -13,7 +13,8 @@ class Test_Juno_Junocam_GoldMaster(unittest.TestCase): #=========================================================================== def setUp(self): - from oops.hosts.juno.junocam import standard_obs +# from oops.hosts.tests.juno.junocam import standard_obs + import standard_obs #=========================================================================== def test_JNCR_2016347_03C00192_V01(self): From 8edd4e80c0405d446fcb754e2fae16224f62a9a1 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Mon, 23 Feb 2026 14:49:17 -0700 Subject: [PATCH 08/21] Fix imports in junocam/standard_obs.py Fix FCPath usage in junocam/__init__.py --- oops/hosts/juno/junocam/__init__.py | 2 +- tests/hosts/juno/junocam/__init__.py | 5 +---- tests/hosts/juno/junocam/standard_obs.py | 4 +--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/oops/hosts/juno/junocam/__init__.py b/oops/hosts/juno/junocam/__init__.py index 8522d10a..04f6d917 100644 --- a/oops/hosts/juno/junocam/__init__.py +++ b/oops/hosts/juno/junocam/__init__.py @@ -118,7 +118,7 @@ def from_file(filespec, fast_distortion=True, # item.insert_subfield('spice_kernels', \ # Juno.used_kernels(item.time, 'junocam', return_all_planets)) item.insert_subfield('filespec', filespec) - item.insert_subfield('basename', os.path.basename(filespec)) + item.insert_subfield('basename', filespec.name) obs.append(item) return obs diff --git a/tests/hosts/juno/junocam/__init__.py b/tests/hosts/juno/junocam/__init__.py index b3ddf00c..b0420084 100644 --- a/tests/hosts/juno/junocam/__init__.py +++ b/tests/hosts/juno/junocam/__init__.py @@ -1,8 +1,6 @@ ################################################################################ # oops/inst/juno/junocam/__init__.py ################################################################################ - -import os import unittest import oops.gold_master as gm @@ -13,8 +11,7 @@ class Test_Juno_Junocam_GoldMaster(unittest.TestCase): #=========================================================================== def setUp(self): -# from oops.hosts.tests.juno.junocam import standard_obs - import standard_obs + from tests.hosts.juno.junocam import standard_obs #=========================================================================== def test_JNCR_2016347_03C00192_V01(self): diff --git a/tests/hosts/juno/junocam/standard_obs.py b/tests/hosts/juno/junocam/standard_obs.py index f050c5dd..4194baa1 100644 --- a/tests/hosts/juno/junocam/standard_obs.py +++ b/tests/hosts/juno/junocam/standard_obs.py @@ -3,9 +3,7 @@ ################################################################################ import os import unittest -import oops.backplane.gold_master as gm - -from oops.unittester_support import TEST_DATA_PREFIX +import oops.gold_master as gm # Because JunoCam has such a large, distorted FOV, we need to assign the # backplanes an especially large inventory border: border=10 seems to work. From cbb650ff22b3e41f81227b3861eabf2d6381274c Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Tue, 28 Apr 2026 16:07:28 -0700 Subject: [PATCH 09/21] convert galileo ssi to use fcpath instead of local path in from _index --- oops/hosts/galileo/ssi/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index 070e93ed..8d9a2549 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -87,15 +87,13 @@ def from_index(filespec, supplemental_filespec=None, full_fov=False, **parameter # Read the index file COLUMNS = [] # Return all columns - local_path = filespec.retrieve(filespec) - table = pdstable.PdsTable(local_path, columns=COLUMNS) + table = pdstable.PdsTable(filespec, columns=COLUMNS) row_dicts = table.dicts_by_row() # Read the supplemental index file if supplemental_filespec is not None: supplemental_filespec = FCPath(supplemental_filespec) - supplemental_local_path = supplemental_filespec.retrieve() - table = pdstable.PdsTable(supplemental_local_path) + table = pdstable.PdsTable(supplemental_filespec) supplemental_row_dicts = table.dicts_by_row() # # Sort supplemental rows to match index file From ae883c63f22a9afba6e07a1570c865506b536ce7 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 29 Apr 2026 13:17:57 -0700 Subject: [PATCH 10/21] Removed FCPath.retrieve where possible --- oops/hosts/cassini/iss.py | 6 ++---- oops/hosts/newhorizons/lorri.py | 3 +-- oops/hosts/voyager/iss.py | 13 ++++--------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/oops/hosts/cassini/iss.py b/oops/hosts/cassini/iss.py index adf31f3d..5fbc310c 100755 --- a/oops/hosts/cassini/iss.py +++ b/oops/hosts/cassini/iss.py @@ -38,8 +38,7 @@ def from_file(filespec, fast_distortion=True, # Load the VICAR file filespec = FCPath(filespec) - local_path = filespec.retrieve() - vic = vicar.VicarImage.from_file(local_path, strict=False) + vic = vicar.VicarImage.from_file(filespec, strict=False) vicar_dict = vic.as_dict() # Get key information from the header @@ -108,8 +107,7 @@ def from_index(filespec, **parameters): # Read the index file COLUMNS = [] # Return all columns TIMES = ['START_TIME'] - local_path = filespec.retrieve() - table = pdstable.PdsTable(local_path, columns=COLUMNS, times=TIMES) + table = pdstable.PdsTable(filespec, columns=COLUMNS, times=TIMES) row_dicts = table.dicts_by_row() # Create a list of Snapshot objects diff --git a/oops/hosts/newhorizons/lorri.py b/oops/hosts/newhorizons/lorri.py index 55adbc24..bb045385 100755 --- a/oops/hosts/newhorizons/lorri.py +++ b/oops/hosts/newhorizons/lorri.py @@ -357,8 +357,7 @@ def from_index(filespec, fov_type='fast', asof=None, meta=None, **parameters): # Read the index file COLUMNS = [] # Return all columns TIMES = ['START_TIME'] # Convert this one to TAI - local_path = filespec.retrieve() - table = pdstable.PdsTable(local_path, columns=COLUMNS, times=TIMES) + table = pdstable.PdsTable(filespec, columns=COLUMNS, times=TIMES) row_dicts = table.dicts_by_row() # Create a list of Snapshot objects diff --git a/oops/hosts/voyager/iss.py b/oops/hosts/voyager/iss.py index ae17a7e9..1eb98462 100755 --- a/oops/hosts/voyager/iss.py +++ b/oops/hosts/voyager/iss.py @@ -36,8 +36,7 @@ def from_file(filespec, astrometry=False, action='error', parameters={}): # Load the PDS label if available if filespec.name.upper().endswith('.LBL'): - local_path = filespec.retrieve() - label_dict = pdsparser.PdsLabel.from_file(local_path).as_dict() + label_dict = pdsparser.Pds3Label(filespec, method='fast').as_dict() imagefile = label_dict['^IMAGE'][0] imagespec = filespec.with_name(imagefile) else: @@ -48,19 +47,16 @@ def from_file(filespec, astrometry=False, action='error', parameters={}): labelspec = filespec.with_suffix('.lbl') try: - local_labelspec = labelspec.retrieve() + label_dict = pdsparser.Pds3Label(labelspec).as_dict() except FileNotFoundError: label_dict = None - else: - label_dict = pdsparser.PdsLabel.from_file(local_labelspec).as_dict() imagespec = filespec # Load the VICAR file vicar_dict = label_dict if not astrometry: - local_imagespec = imagespec.retrieve() - vic = vicar.VicarImage.from_file(local_imagespec) + vic = vicar.VicarImage.from_file(imagespec) vicar_dict = vic.as_dict() # Get key information, preferably from the PDS label @@ -211,8 +207,7 @@ def from_index(filespec, geomed=False, action='ignore', omit=True, # Read the index file COLUMNS = [] # Return all columns - local_path = filespec.retrieve() - table = pdstable.PdsTable(local_path, columns=COLUMNS) + table = pdstable.PdsTable(filespec, columns=COLUMNS) row_dicts = table.dicts_by_row() # Interpret GEOMED parameter From 9b9ad98662f47594db221e3ac9e725afb5ce4cab Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 29 Apr 2026 13:32:44 -0700 Subject: [PATCH 11/21] Removed FCPath.retrieve from jiram modules. --- oops/hosts/juno/jiram/__init__.py | 10 +++------- oops/hosts/juno/junocam/__init__.py | 9 ++------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/oops/hosts/juno/jiram/__init__.py b/oops/hosts/juno/jiram/__init__.py index 01e8fad0..459e6041 100644 --- a/oops/hosts/juno/jiram/__init__.py +++ b/oops/hosts/juno/jiram/__init__.py @@ -30,11 +30,7 @@ def from_file(filespec, return_all_planets=False, **parameters): # Load the PDS label filespec = FCPath(filespec) - lbl_filespec = filespec.with_suffix('.LBL') - local_filespec = filespec.retrieve() - local_lbl_filespec = lbl_filespec.retrieve() - recs = pdsparser.PdsLabel.load_file(local_lbl_filespec) - label = pdsparser.PdsLabel.from_string(recs).as_dict() + label = pdsparser.Pds3Label(filespec).as_dict() # Get common metadata meta = Metadata(label) @@ -49,13 +45,13 @@ def from_file(filespec, return_all_planets=False, **parameters): # Image if ext.upper() == '.IMG': from . import img - return img.from_file(local_filespec, label, + return img.from_file(filespec, label, return_all_planets=False, **parameters) # Spectrum if ext.upper() == '.DAT': from . import spe - return spe.from_file(local_filespec, label, + return spe.from_file(filespec, label, return_all_planets=False, **parameters) return None diff --git a/oops/hosts/juno/junocam/__init__.py b/oops/hosts/juno/junocam/__init__.py index 04f6d917..d741e8d9 100644 --- a/oops/hosts/juno/junocam/__init__.py +++ b/oops/hosts/juno/junocam/__init__.py @@ -47,14 +47,9 @@ def from_file(filespec, fast_distortion=True, ### I think we recommend a blank line after a multi-line docstring. filespec = FCPath(filespec) + # Load the PDS label - lbl_filespec = filespec.with_suffix('.LBL') -### This failed for me because the files come off the PDS archive volumes in -### upper case, so they end in '.IMG', not 'img'. You need to find a way to make -### this work regardless of the case of either file extension. - local_lbl_filespec = lbl_filespec.retrieve() - recs = pdsparser.PdsLabel.load_file(local_lbl_filespec) - label = pdsparser.PdsLabel.from_string(recs).as_dict() + label = pdsparser.Pds3Label(filespec).as_dict() # Get composite image metadata meta = Metadata(label) From c7143ee32a712d1b40d052513705ea7fd9aaadd2 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Thu, 30 Apr 2026 15:06:51 -0700 Subject: [PATCH 12/21] coderabbit --- oops/hosts/cassini/uvis.py | 5 +++-- oops/hosts/cassini/vims.py | 5 +++-- oops/hosts/galileo/ssi/__init__.py | 5 +++-- oops/hosts/juno/jiram/__init__.py | 9 +++++---- oops/hosts/juno/junocam/__init__.py | 7 ++++--- oops/hosts/voyager/iss.py | 12 ++++++------ 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/oops/hosts/cassini/uvis.py b/oops/hosts/cassini/uvis.py index a26c5c0e..56290211 100755 --- a/oops/hosts/cassini/uvis.py +++ b/oops/hosts/cassini/uvis.py @@ -21,7 +21,7 @@ # Standard class methods ########################################################################################## -def from_file(filespec, data=True, enclose=False, **parameters): +def from_file(filespec, data=True, enclose=False, method='strict', **parameters): """A general, static method to return one or more Observation subclass objects based on a label for a given Cassini UVIS file. @@ -34,13 +34,14 @@ def from_file(filespec, data=True, enclose=False, **parameters): enclosing limits in line and band, and the binning is assumed to be 1. If False and multiple windows are used, the function returns a tuple of observations rather than a single observation. + method: Label reading method to be passed to Pds3Label. """ UVIS.initialize() # Define everything the first time through; use defaults unless # initialize() is called explicitly. # Get the label dictionary and data array dimensions - label = pdsparser.Pds3Label(filespec, method='fast').as_dict() + label = pdsparser.Pds3Label(filespec, method=method).as_dict() # Load any needed SPICE kernels tstart = julian.tdb_from_tai(julian.tai_from_iso(label['START_TIME'])) diff --git a/oops/hosts/cassini/vims.py b/oops/hosts/cassini/vims.py index 0ee64460..db39c658 100755 --- a/oops/hosts/cassini/vims.py +++ b/oops/hosts/cassini/vims.py @@ -189,7 +189,7 @@ # Standard class methods ########################################################################################## -def from_file(filespec, data=True): +def from_file(filespec, method='strict', data=True): """A general, static method to return a pair of Observation objects based on a given Cassini VIMS data file or label file. @@ -198,6 +198,7 @@ def from_file(filespec, data=True): data if True, data arrays are included in the returned observation objects. Use a tuple of two booleans to specify whether to include the VIS and IR data independently. + method Label reading method to be passed to Pds3Label. Return: (vis, ir) vis the VIS observation, or None if the VIS channel was inactive. @@ -212,7 +213,7 @@ def from_file(filespec, data=True): filespec = FCPath(filespec) - label = pdsparser.Pds3Label(filespec, method='fast').as_dict() + label = pdsparser.Pds3Label(filespec, method=method).as_dict() # Insert "data_file" and "header_recs" # Convert ISIS .qub info to a standard PDS3 label dictionary diff --git a/oops/hosts/galileo/ssi/__init__.py b/oops/hosts/galileo/ssi/__init__.py index 8d9a2549..f5eb5a00 100755 --- a/oops/hosts/galileo/ssi/__init__.py +++ b/oops/hosts/galileo/ssi/__init__.py @@ -20,7 +20,7 @@ # Standard class methods ################################################################################ def from_file(filespec, - return_all_planets=False, full_fov=False, **parameters): + return_all_planets=False, full_fov=False, method='strict', **parameters): """A general, static method to return a Snapshot object based on a given Galileo SSI image file. By default, only the valid image region is returned. @@ -33,6 +33,7 @@ def from_file(filespec, full_fov: If True, the full image is returned with a mask describing the regions with no data. + method: Label reading method to be passed to Pds3Label. """ SSI.initialize() # Define everything the first time through; use defaults @@ -41,7 +42,7 @@ def from_file(filespec, filespec = FCPath(filespec) # Load the PDS label - label = pdsparser.Pds3Label(filespec, method='fast').as_dict() + label = pdsparser.Pds3Label(filespec, method=method).as_dict() # Load the data array vic = vicar.VicarImage.from_file(filespec) diff --git a/oops/hosts/juno/jiram/__init__.py b/oops/hosts/juno/jiram/__init__.py index 459e6041..76e1faa3 100644 --- a/oops/hosts/juno/jiram/__init__.py +++ b/oops/hosts/juno/jiram/__init__.py @@ -17,20 +17,21 @@ ################################################################################ # Standard class methods ################################################################################ -def from_file(filespec, return_all_planets=False, **parameters): +def from_file(filespec, return_all_planets=False, method='strict', **parameters): """A general, static method to return a Snapshot object based on a given JIRAM image or spectrum file. Inputs: return_all_planets Include kernels for all planets not just Jupiter or Saturn. + method Label reading method to be passed to Pds3Label. """ JIRAM.initialize() # Define everything the first time through; use # defaults unless initialize() is called explicitly. # Load the PDS label filespec = FCPath(filespec) - label = pdsparser.Pds3Label(filespec).as_dict() + label = pdsparser.Pds3Label(filespec, method=method).as_dict() # Get common metadata meta = Metadata(label) @@ -46,13 +47,13 @@ def from_file(filespec, return_all_planets=False, **parameters): if ext.upper() == '.IMG': from . import img return img.from_file(filespec, label, - return_all_planets=False, **parameters) + return_all_planets=return_all_planets, **parameters) # Spectrum if ext.upper() == '.DAT': from . import spe return spe.from_file(filespec, label, - return_all_planets=False, **parameters) + return_all_planets=return_all_planets, **parameters) return None diff --git a/oops/hosts/juno/junocam/__init__.py b/oops/hosts/juno/junocam/__init__.py index d741e8d9..b6d7ee3d 100644 --- a/oops/hosts/juno/junocam/__init__.py +++ b/oops/hosts/juno/junocam/__init__.py @@ -1,5 +1,5 @@ ################################################################################ -# oops/inst/juno/junocam.py +# oops/inst/juno/junocam/__init__.py ################################################################################ import re @@ -25,7 +25,7 @@ #=============================================================================== ### Avoid two horizontal separators in a row. One should suffice. def from_file(filespec, fast_distortion=True, - return_all_planets=False, snap=False, **parameters): + return_all_planets=False, snap=False, method='strict', **parameters): """A general, static method to return a Pushframe object based on a given JUNOCAM image file. @@ -41,6 +41,7 @@ def from_file(filespec, fast_distortion=True, snap True to model the image as a Snapshot rather than as a TimedImage. + method Label reading method to be passed to Pds3Label. """ JUNOCAM.initialize() # Define everything the first time through; use # defaults unless initialize() is called explicitly. @@ -49,7 +50,7 @@ def from_file(filespec, fast_distortion=True, filespec = FCPath(filespec) # Load the PDS label - label = pdsparser.Pds3Label(filespec).as_dict() + label = pdsparser.Pds3Label(filespec, method=method).as_dict() # Get composite image metadata meta = Metadata(label) diff --git a/oops/hosts/voyager/iss.py b/oops/hosts/voyager/iss.py index 1eb98462..20a47958 100755 --- a/oops/hosts/voyager/iss.py +++ b/oops/hosts/voyager/iss.py @@ -18,8 +18,7 @@ ################################################################################ # Standard class methods ################################################################################ - -def from_file(filespec, astrometry=False, action='error', parameters={}): +def from_file(filespec, astrometry=False, action='error', method='strict', parameters={}): """A general, static method to return a Snapshot object based on a given Voyager ISS image file or its label. @@ -29,6 +28,7 @@ def from_file(filespec, astrometry=False, action='error', parameters={}): action What to do for a missing C kernel entry, via the Python warnings interface: 'error', 'ignore', 'always', 'default', 'module', 'once'. + method Label reading method to be passed to Pds3Label. """ ISS.initialize() # Define everything the first time through @@ -36,7 +36,7 @@ def from_file(filespec, astrometry=False, action='error', parameters={}): # Load the PDS label if available if filespec.name.upper().endswith('.LBL'): - label_dict = pdsparser.Pds3Label(filespec, method='fast').as_dict() + label_dict = pdsparser.Pds3Label(filespec, method=method).as_dict() imagefile = label_dict['^IMAGE'][0] imagespec = filespec.with_name(imagefile) else: @@ -105,7 +105,7 @@ def from_file(filespec, astrometry=False, action='error', parameters={}): factor = None # Interpret the GEOMED parameter - if 'GEOMA' in vic['TASK+']: + if (not astrometry) and ('GEOMA' in vic['TASK+']): assert vic.data_2d.shape == (1000,1000) fovs = { 'NAC': ISS.fovs['NAC_GEOMED'], @@ -169,8 +169,8 @@ def from_file(filespec, astrometry=False, action='error', parameters={}): result = oops.obs.Snapshot(('v','u'), tstart, texp, fovs[camera], path = spacecraft, frame = image_frame, - dict = vicar_dict, # Add the VICAR dict - data = vic.data_2d, # Add the data array + dict = vicar_dict, # Add the VICAR dict + data = (None if astrometry else vic.data_2d), # Add the data array instrument = 'ISS', detector = camera, filter = filter, From 39edf535cc55679e5d8b7b797c5608a7f811e0c6 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Sat, 2 May 2026 07:48:19 -0700 Subject: [PATCH 13/21] Propagate method argument in second call to Pds3Label in voyager/iss.py --- oops/hosts/cassini/vims.py | 2 +- oops/hosts/voyager/iss.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oops/hosts/cassini/vims.py b/oops/hosts/cassini/vims.py index db39c658..8767043d 100755 --- a/oops/hosts/cassini/vims.py +++ b/oops/hosts/cassini/vims.py @@ -189,7 +189,7 @@ # Standard class methods ########################################################################################## -def from_file(filespec, method='strict', data=True): +def from_file(filespec, data=True, method='strict'): """A general, static method to return a pair of Observation objects based on a given Cassini VIMS data file or label file. diff --git a/oops/hosts/voyager/iss.py b/oops/hosts/voyager/iss.py index 20a47958..39087480 100755 --- a/oops/hosts/voyager/iss.py +++ b/oops/hosts/voyager/iss.py @@ -47,7 +47,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param labelspec = filespec.with_suffix('.lbl') try: - label_dict = pdsparser.Pds3Label(labelspec).as_dict() + label_dict = pdsparser.Pds3Label(labelspe, method=method).as_dict() except FileNotFoundError: label_dict = None From b16102c123d1a42256a4f7bd23c28aae4a6fe6cb Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Sat, 2 May 2026 08:50:45 -0700 Subject: [PATCH 14/21] coderabbit --- oops/hosts/voyager/iss.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/oops/hosts/voyager/iss.py b/oops/hosts/voyager/iss.py index 39087480..938f8f49 100755 --- a/oops/hosts/voyager/iss.py +++ b/oops/hosts/voyager/iss.py @@ -18,7 +18,7 @@ ################################################################################ # Standard class methods ################################################################################ -def from_file(filespec, astrometry=False, action='error', method='strict', parameters={}): +def from_file(filespec, astrometry=False, action='error', method='strict', parameters=None): """A general, static method to return a Snapshot object based on a given Voyager ISS image file or its label. @@ -28,8 +28,12 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param action What to do for a missing C kernel entry, via the Python warnings interface: 'error', 'ignore', 'always', 'default', 'module', 'once'. + parameters: Dictionary of VGR-ISS-specific parameters. method Label reading method to be passed to Pds3Label. """ + if not parameters: + parameters={} + ISS.initialize() # Define everything the first time through filespec = FCPath(filespec) @@ -47,7 +51,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param labelspec = filespec.with_suffix('.lbl') try: - label_dict = pdsparser.Pds3Label(labelspe, method=method).as_dict() + label_dict = pdsparser.Pds3Label(labelspec, method=method).as_dict() except FileNotFoundError: label_dict = None From 5116ca16f50027fea0048c9c1a00c968c301a3e7 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Sat, 2 May 2026 09:25:49 -0700 Subject: [PATCH 15/21] coderabbit --- oops/hosts/voyager/iss.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/oops/hosts/voyager/iss.py b/oops/hosts/voyager/iss.py index 938f8f49..c1c7facc 100755 --- a/oops/hosts/voyager/iss.py +++ b/oops/hosts/voyager/iss.py @@ -64,7 +64,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param vicar_dict = vic.as_dict() # Get key information, preferably from the PDS label - if label_dict is not None: + if label_dict: stop_time = label_dict['STOP_TIME'] texp = max(1.e-6, label_dict['EXPOSURE_DURATION']) @@ -83,7 +83,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param target = label_dict['TARGET_NAME'] filter = label_dict['FILTER_NAME'] factor = label_dict['IMAGE']['REFLECTANCE_SCALING_FACTOR'] - else: + else if vicar_dict: lab02 = vicar_dict['LAB02'] lab03 = vicar_dict['LAB03'] stop_time = '19%s-%sT%s' % (lab02[47:49],lab02[50:53],lab02[54:62]) @@ -107,6 +107,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param filter = lab03[37:43].rstrip() factor = None + else return None # Interpret the GEOMED parameter if (not astrometry) and ('GEOMA' in vic['TASK+']): @@ -183,7 +184,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param filespec = filespec, basename = filespec.name) - # TODO if factor is not None: + # TODO if factor: # result.insert_subfield('extended_calib', # oops.calib.ExtendedSource('I/F', factor)) From 5fcaf76b07eac84a02df96d3098f46b11f22004d Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Mon, 4 May 2026 13:08:11 -0700 Subject: [PATCH 16/21] Rob comments --- oops/hosts/voyager/iss.py | 11 ++++++----- spicedb/__init__.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/oops/hosts/voyager/iss.py b/oops/hosts/voyager/iss.py index c1c7facc..5fbf3faa 100755 --- a/oops/hosts/voyager/iss.py +++ b/oops/hosts/voyager/iss.py @@ -28,10 +28,10 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param action What to do for a missing C kernel entry, via the Python warnings interface: 'error', 'ignore', 'always', 'default', 'module', 'once'. - parameters: Dictionary of VGR-ISS-specific parameters. + parameters Dictionary of VGR-ISS-specific parameters. method Label reading method to be passed to Pds3Label. """ - if not parameters: + if parameters is None: parameters={} ISS.initialize() # Define everything the first time through @@ -64,7 +64,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param vicar_dict = vic.as_dict() # Get key information, preferably from the PDS label - if label_dict: + if label_dict is not None: stop_time = label_dict['STOP_TIME'] texp = max(1.e-6, label_dict['EXPOSURE_DURATION']) @@ -83,7 +83,7 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param target = label_dict['TARGET_NAME'] filter = label_dict['FILTER_NAME'] factor = label_dict['IMAGE']['REFLECTANCE_SCALING_FACTOR'] - else if vicar_dict: + elif vicar_dict is not None: lab02 = vicar_dict['LAB02'] lab03 = vicar_dict['LAB03'] stop_time = '19%s-%sT%s' % (lab02[47:49],lab02[50:53],lab02[54:62]) @@ -107,7 +107,8 @@ def from_file(filespec, astrometry=False, action='error', method='strict', param filter = lab03[37:43].rstrip() factor = None - else return None + else: + return None # Interpret the GEOMED parameter if (not astrometry) and ('GEOMA' in vic['TASK+']): diff --git a/spicedb/__init__.py b/spicedb/__init__.py index f02ddfa4..41c0bfe1 100755 --- a/spicedb/__init__.py +++ b/spicedb/__init__.py @@ -3530,4 +3530,4 @@ def translator2(filepath): ################################################################################ if __name__ == '__main__': unittest.main(verbosity=2) -################################################################################ +################################################################################ \ No newline at end of file From 6bf91fbe6c791f381430282651fdcf264e2a4585 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 6 May 2026 09:12:13 -0700 Subject: [PATCH 17/21] Remove explicit use of filecache.retrieve() Use filecache.replace() --- oops/gold_master/__init__.py | 55 +++++++++++------------------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/oops/gold_master/__init__.py b/oops/gold_master/__init__.py index cc31845c..85576263 100644 --- a/oops/gold_master/__init__.py +++ b/oops/gold_master/__init__.py @@ -1147,31 +1147,19 @@ def run_tests(self): log_path = (BACKPLANE_OUTPUT_PREFIX / self.output_dir / f'{self.task}.log') - localpath = None - try: - localpath = log_path.retrieve() - except FileNotFoundError: - pass - - if localpath: + localpath = log_path.get_local_path() + if os.path.exists(localpath): + # Append the latest modification date to the pre-existing file dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath)) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') - dated_localpath = localpath[:-4] + suffix + '.log' dated_logpath = log_path[:-4] + suffix + '.log' - # Note that for a cloud destination, this doesn't actually delete - # the original summary.py file in the cloud, since you can't rename - # files in the cloud. Instead we upload a copy with the new dated - # name and then the write below will overwrite the old version. - # Also notice that this attempt to use the modification date of the - # original summary.py file won't work in the cloud, because - # creation/modification times are not preserved when a file is - # retrieved. Instead, it will use the time the file was downloaded. - os.rename(localpath, dated_localpath) - (BACKPLANE_OUTPUT_PREFIX / dated_logpath).upload() - - abs_log_path = log_path.get_local_path() - handler = logging.FileHandler(abs_log_path) + + # Move or copy the old log to its timestamped filename using + # filecache-aware replacement semantics. + log_path.replace(dated_logpath) + + handler = logging.FileHandler(localpath) LOGGING.logger.addHandler(handler) # Run the tests @@ -2325,28 +2313,17 @@ def write_summary(self, outdir): filepath = outdir / 'summary.py' - localpath = None - try: - localpath = str(filepath.retrieve(filepath)) - except FileNotFoundError: - pass - - if localpath: + localpath = filepath.get_local_path() + if os.path.exists(localpath): + # Append the latest modification date to any pre-existing file dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath)) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') - dated_localpath = localpath[:-3] + suffix + '.py' dated_filepath = filepath[:-3] + suffix + '.py' - # Note that for a cloud destination, this doesn't actually delete - # the original summary.py file in the cloud, since you can't rename - # files in the cloud. Instead we upload a copy with the new dated - # name and then the write below will overwrite the old version. - # Also notice that this attempt to use the modification date of the - # original summary.py file won't work in the cloud, because - # creation/modification times are not preserved when a file is - # retrieved. Instead, it will use the time the file was downloaded. - os.rename(localpath, dated_localpath) - dated_filepath.upload() + + # Move or copy the old summary to its timestamped filename using + # filecache-aware replacement semantics. + filepath.replace(dated_filepath) LOGGING.info('Previous summary moved to:', dated_filepath.name) titles = list(self.summary.keys()) From 8d5136ebae9d319e68cd4ce1d01334fb8dae15e9 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Wed, 6 May 2026 13:56:54 -0700 Subject: [PATCH 18/21] coderabbit --- oops/gold_master/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/oops/gold_master/__init__.py b/oops/gold_master/__init__.py index 85576263..b1b0d8b3 100644 --- a/oops/gold_master/__init__.py +++ b/oops/gold_master/__init__.py @@ -1151,7 +1151,8 @@ def run_tests(self): if os.path.exists(localpath): # Append the latest modification date to the pre-existing file - dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath)) + dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath), + tz=datetime.timezone.utc) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') dated_logpath = log_path[:-4] + suffix + '.log' @@ -1159,7 +1160,7 @@ def run_tests(self): # filecache-aware replacement semantics. log_path.replace(dated_logpath) - handler = logging.FileHandler(localpath) + handler = logging.FileHandler(localpath, mode='w') LOGGING.logger.addHandler(handler) # Run the tests @@ -2317,7 +2318,8 @@ def write_summary(self, outdir): if os.path.exists(localpath): # Append the latest modification date to any pre-existing file - dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath)) + dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath), + tz=datetime.timezone.utc) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') dated_filepath = filepath[:-3] + suffix + '.py' From ce624f41bf7cb9fa23ad4078ff39082c42a3a408 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Thu, 7 May 2026 12:37:43 -0700 Subject: [PATCH 19/21] use filecache.exists(), filecache.modification_time() gold_master --- oops/gold_master/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/oops/gold_master/__init__.py b/oops/gold_master/__init__.py index b1b0d8b3..36574250 100644 --- a/oops/gold_master/__init__.py +++ b/oops/gold_master/__init__.py @@ -1147,11 +1147,10 @@ def run_tests(self): log_path = (BACKPLANE_OUTPUT_PREFIX / self.output_dir / f'{self.task}.log') - localpath = log_path.get_local_path() - if os.path.exists(localpath): + if log_path.exists(): # Append the latest modification date to the pre-existing file - dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath), + dt = datetime.datetime.fromtimestamp(log_path.modification_time(), tz=datetime.timezone.utc) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') dated_logpath = log_path[:-4] + suffix + '.log' @@ -1160,7 +1159,7 @@ def run_tests(self): # filecache-aware replacement semantics. log_path.replace(dated_logpath) - handler = logging.FileHandler(localpath, mode='w') + handler = logging.FileHandler(logpath.get_localpath(), mode='w') LOGGING.logger.addHandler(handler) # Run the tests @@ -2314,11 +2313,10 @@ def write_summary(self, outdir): filepath = outdir / 'summary.py' - localpath = filepath.get_local_path() - if os.path.exists(localpath): + if log_path.exists(): # Append the latest modification date to any pre-existing file - dt = datetime.datetime.fromtimestamp(os.path.getmtime(localpath), + dt = datetime.datetime.fromtimestamp(log_path.modification_time(), tz=datetime.timezone.utc) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') dated_filepath = filepath[:-3] + suffix + '.py' From 9b3005d89f2d64cac09c64013f5d33f556820e78 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Thu, 7 May 2026 13:25:49 -0700 Subject: [PATCH 20/21] coderabbit --- oops/gold_master/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oops/gold_master/__init__.py b/oops/gold_master/__init__.py index 36574250..9f766b29 100644 --- a/oops/gold_master/__init__.py +++ b/oops/gold_master/__init__.py @@ -1159,7 +1159,7 @@ def run_tests(self): # filecache-aware replacement semantics. log_path.replace(dated_logpath) - handler = logging.FileHandler(logpath.get_localpath(), mode='w') + handler = logging.FileHandler(logpath.as_posix(), mode='w') LOGGING.logger.addHandler(handler) # Run the tests From a261676a0e6c9af108af8a03ff42f6c9bc71edb4 Mon Sep 17 00:00:00 2001 From: Joseph Spitale Date: Thu, 7 May 2026 13:29:18 -0700 Subject: [PATCH 21/21] coderabbit --- oops/gold_master/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oops/gold_master/__init__.py b/oops/gold_master/__init__.py index 9f766b29..3a64793a 100644 --- a/oops/gold_master/__init__.py +++ b/oops/gold_master/__init__.py @@ -2313,10 +2313,10 @@ def write_summary(self, outdir): filepath = outdir / 'summary.py' - if log_path.exists(): + if filepath.exists(): # Append the latest modification date to any pre-existing file - dt = datetime.datetime.fromtimestamp(log_path.modification_time(), + dt = datetime.datetime.fromtimestamp(filepath.modification_time(), tz=datetime.timezone.utc) suffix = dt.strftime('-%Y-%m-%dT%H-%M-%S') dated_filepath = filepath[:-3] + suffix + '.py'