Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ jobs:
matrix:
# MacOS: Python 3.8-3.10 does not currently work on MacOS.
include:
- os: self-hosted-linux
python-version: "3.9"
- os: self-hosted-linux
python-version: "3.10"
- os: self-hosted-linux
Expand All @@ -34,8 +32,6 @@ jobs:
python-version: "3.12"
- os: self-hosted-macos
python-version: "3.13"
- os: self-hosted-windows
python-version: "3.9"
- os: self-hosted-windows
python-version: "3.10"
- os: self-hosted-windows
Expand Down
6 changes: 5 additions & 1 deletion oops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@

from oops.backplane import Backplane
from oops.body import Body
from oops.cache import Cache
from oops.event import Event
from oops.fittable import Fittable
from oops.fittable import Fittable, Fittable_
from oops.meshgrid import Meshgrid
from oops.transform import Transform

Expand Down Expand Up @@ -82,6 +83,9 @@
frame.Frame.J2000,
path.Path.SSB)

Cache.FRAME_CLASS = Frame
Cache.PATH_CLASS = Path

Frame.EVENT_CLASS = Event
Frame.PATH_CLASS = Path
Frame.SPICEPATH_CLASS = oops.path.SpicePath
Expand Down
37 changes: 21 additions & 16 deletions oops/backplane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

class Backplane(object):
"""Class that supports the generation and manipulation of sets of backplanes
with a particular observation.
with a particular Observation.

intermediate results are cached to speed up calculations.
Intermediate results are cached to speed up calculations.
"""

DIAGNOSTICS = False # set True to log diagnostics
Expand Down Expand Up @@ -118,15 +118,7 @@ def __init__(self, obs, meshgrid=None, time=None, inventory=None,
else:
self.meshgrid = meshgrid

if time is None:
self.time = obs.timegrid(self.meshgrid)
else:
self.time = Scalar.as_scalar(time)

# For some cases, times are all equal. If so, collapse the times.
dt = self.time - obs.midtime
if abs(dt).max() < 1.e-3: # simplifies cases with jitter in time tags
self.time = Scalar(obs.midtime)
self._input_time = time

# Intialize the inventory
self._input_inventory = inventory
Expand All @@ -139,19 +131,32 @@ def __init__(self, obs, meshgrid=None, time=None, inventory=None,

self.inventory_border = inventory_border

self._refresh() # Fill in all internals

def _refresh(self):

if self._input_time is None:
self.time = self.obs.timegrid(self.meshgrid)
else:
self.time = Scalar.as_scalar(self._input_time)

# For some cases, times are all equal. If so, collapse the times.
dt = self.time - self.obs.midtime
if abs(dt).max() < 1.e-3: # simplifies cases with jitter in time tags
self.time = Scalar(self.obs.midtime)

# Define events
self.obs_event = obs.event_at_grid(self.meshgrid, time=self.time)
self.obs_event = self.obs.event_at_grid(self.meshgrid, time=self.time)
self.shape = self.obs_event.shape

# dict[derivs] = event
self.obs_events = {
False: self.obs_event.wod,
True : self.obs_event.with_los_derivs()
}

self.obs_gridless_event = obs.gridless_event(self.meshgrid,
time=self.time)

self.shape = self.obs_event.shape
self.obs_gridless_event = self.obs.gridless_event(self.meshgrid,
time=self.time)

# The surface_events dictionary comes in two versions, with and without
# derivatives with respect to los and time.
Expand Down
117 changes: 117 additions & 0 deletions oops/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
##########################################################################################
# oops/cache.py: Support for caching of OOPS objects
##########################################################################################

import numpy as np
from polymath import Qube


class Cache:
"""Class that can be indexed like a dictionary, where `maxsize` items are preserved.
When the size of the cache exceeds `maxsize` by ~ 10%, the least-recently accessed
items are deleted.
Indexing a Cache using a key that is not present, or has been deleted, returns None.
A KeyError is never raised.
Dictionary keys can include mutable items, which are converted to immutable. The class
method `clean_key` performs this conversion.
"""

# These are filled in by oops/__init__.py to avoid circular imports
FRAME_CLASS = None
PATH_CLASS = None

def __init__(self, maxsize=100):
"""Constructor for a Cache.
Parameters:
maxsize (int, optional): The rough limit on the number of items stored in the
Cache. When this value is exceeded by ~ 10%, the number of elements is
reduced back to `maxsize` by removing the items accessed least recently.
"""

self._maxsize = maxsize
self._extras = max(3, maxsize//10)
self._limit = maxsize + self._extras
self._dict = {}
self._counter = 0

def __len__(self):
"""The number of items currently in this Cache."""
return len(self._dict)

@staticmethod
def clean_key(key):
"""Convert the given key to immutable so it can be used as a dictionary key."""

def clean_item(item):
match item:
case Qube():
vals = tuple(item.vals.ravel()) if np.shape(item.vals) else item.vals
mask = tuple(item.mask.ravel()) if np.shape(item.mask) else item.mask
return (type(item).__name__, item.shape, vals, mask)
case np.ndarray():
return (item.shape, tuple(item.ravel()))
case Cache.PATH_CLASS():
return Cache.PATH_CLASS.as_waypoint(item)
case Cache.FRAME_CLASS():
return Cache.FRAME_CLASS.as_wayframe(item)
case x if hasattr(x, '__data__'):
return id(item)
case list():
return tuple(item)
case _:
return item

if isinstance(key, (list, tuple)):
return tuple(clean_item(item) for item in key)

return clean_item(key)

def __contains__(self, key):
"""True if the given key is currently in the Cache."""

if self._maxsize:
key = Cache.clean_key(key)
if key in self._dict:
self._counter += 1
self._dict[key][0] = self._counter
return True
return False

def __getitem__(self, key):
"""The value associated with the given key, or None if the key is missing.
Supports index notation using square brackets "[]".
"""

if self._maxsize:
key = Cache.clean_key(key)
if key in self._dict:
self._counter += 1
count_key_value = self._dict[key]
count_key_value[0] = self._counter
return count_key_value[2]

return None

def __setitem__(self, key, value):
"""Set the value associated with the given key.
Supports index notation using square brackets "[]".
"""

if self._maxsize:
key = Cache.clean_key(key)
self._counter += 1
self._dict[key] = [self._counter, key, value]

if len(self._dict) > self._limit:
tuples = list(self._dict.values())
tuples.sort()
extras = tuples[:-self._maxsize]
for (_, k, _) in extras:
del self._dict[k]

##########################################################################################
1 change: 1 addition & 0 deletions oops/cadence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from oops.cadence.reshapedcadence import ReshapedCadence
from oops.cadence.reversedcadence import ReversedCadence
from oops.cadence.sequence import Sequence
from oops.cadence.snapcadence import SnapCadence
from oops.cadence.tdicadence import TDICadence

################################################################################
14 changes: 14 additions & 0 deletions oops/cadence/dualcadence.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,25 @@ def __init__(self, long, short):

self._max_long_tstep = self.long.shape[0] - 1

def _refresh(self):
"""Update internals if self.long or self.short is Fittable."""
self.time = (self.long.time[0],
self.long.lasttime + self.short.time[1])
self.midtime = (self.time[0] + self.time[1]) * 0.5
self.lasttime = self.long.lasttime + self.short.lasttime

def __getstate__(self):
self.refresh()
return (self.long, self.short)

def __setstate__(self, state):
self.__init__(*state)
self.freeze()

self.time = (self.long.time[0],
self.long.lasttime + self.short.time[1])
self.midtime = (self.time[0] + self.time[1]) * 0.5
self.lasttime = self.long.lasttime + self.short.lasttime

#===========================================================================
def time_at_tstep(self, tstep, remask=False, derivs=False, inclusive=True):
Expand Down
15 changes: 1 addition & 14 deletions oops/cadence/metronome.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ def __init__(self, tstart, tstride, texp, steps, clip=True):
self._max_step = self.steps - 1

def __getstate__(self):
return (self.tstart, self.tstride, self.texp, self.steps,
self.clip)
return (self.tstart, self.tstride, self.texp, self.steps, self.clip)

def __setstate__(self, state):
self.__init__(*state)
Expand Down Expand Up @@ -366,16 +365,4 @@ def for_array1d(steps, tstart, texp, interstep_delay=0.):

return Metronome(tstart, texp + interstep_delay, texp, steps)

#===========================================================================
@staticmethod
def for_array0d(tstart, texp):
"""Alternative constructor for a product with no time-axis.

Input:
tstart start time in seconds TDB.
texp exposure duration in seconds.
"""

return Metronome(tstart, texp, texp, 1)

################################################################################
8 changes: 8 additions & 0 deletions oops/cadence/reshapedcadence.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,19 @@ def __init__(self, cadence, shape):
self._old_rank = len(self.cadence.shape)
self._old_stride = np.cumprod((self._old_shape + (1,))[::-1])[-2::-1]

def _refresh(self):
"""Update internals if self.cadence is Fittable."""
self.time = self.cadence.time
self.midtime = self.cadence.midtime
self.lasttime = self.cadence.lasttime

def __getstate__(self):
self.refresh()
return (self.cadence, self.shape)

def __setstate__(self, state):
self.__init__(*state)
self.freeze()

#===========================================================================
@staticmethod
Expand Down
6 changes: 6 additions & 0 deletions oops/cadence/reversedcadence.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ def __init__(self, cadence, axis=0):
self._first_time = self.cadence.time_range_at_tstep(self._max_step)[0]
self._last_time = self.cadence.time_range_at_tstep(0)[1]

def _refresh(self):
"""Update internals if self.cadence is Fittable."""
self.time = self.cadence.time
self.midtime = self.cadence.midtime
self.lasttime = self.cadence.lasttime

def __getstate__(self):
return (self.cadence,)

Expand Down
30 changes: 30 additions & 0 deletions oops/cadence/snapcadence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
################################################################################
# oops/cadence/metronome.py: Metronome subclass of class Cadence
################################################################################

from oops.cadence import Metronome

class SnapCadence(Metronome):
"""A shapeless Cadence subclass with a single start and stop."""

def __init__(self, tstart, texp, clip=True):
"""Constructor for a SnapCadence.
Input:
tstart the start time of the observation in seconds TDB.
texp the exposure time in seconds associated with each step.
This may be shorter than tstride due to readout times,
etc. It may also be longer.
clip if True (the default), times and index values are always
clipped into the valid range.
"""

Metronome.__init__(self, tstart, texp, texp, 1, clip=clip)

def __getstate__(self):
return (self.tstart, self.texp, self.clip)

def __setstate__(self, state):
self.__init__(*state)

################################################################################
Loading