Add Fittable/mutable framework, rewrite the Path and Frame registries, require Python 3.11 - #203
Add Fittable/mutable framework, rewrite the Path and Frame registries, require Python 3.11#203markshowalter wants to merge 15 commits into
Conversation
The main unit test suite goes from 19 failing tests to zero, and the host gold-master suite from four of five failing to zero. Library fixes: * mutable: _refresh_internal never applied _refresh() on an object's first pass, so the mutable.refresh(self) at the end of every constructor did nothing and derived attributes such as _transform, _times and _pos_x were never created. _needs_refresh_internal read two attributes that do not exist. * path_: five utility classes cached themselves in Frame._FRAME_CACHE instead of Path._PATH_CACHE, leaving the path cache unused and the frame cache polluted with Paths; _wrt lacked the reversal branch its Frame counterpart has, so linking a root path to one of its descendants recursed until the stack overflowed; _register omitted the two-element cache key; RelativePath discarded the origin Path it needs for the subtraction and advertised the frame of the wrong Path. * frame_: the LinkedFrame origin check rejected a null frame origin, which is the ordinary case rather than an error. * quickpath, quickframe: restored the "already quick" guards dropped in the rewrite, and repaired extend() -- a dict read as a method, frame keys used in a path class, a stale _steps, arrays whose lengths disagreed, a seam that left the times non-monotonic, and a tuple passed to a two-argument signature. * spicepath: get() ignored origin and frame when asked for the SSB. SPICE accepts several names per body, so the name a caller uses is now registered alongside the canonical one; "GLL" resolves to GALILEO_ORBITER. * keplerpath: eleven attribute names left behind by an incomplete privatization. * hosts/juno: os.path.basename() applied to an FCPath, which is not os.PathLike; use the FCPath.name property, as the other hosts do. Tests were updated for the current APIs. Two were also unsound: SpicePath tests restored Path._USE_QUICKPATHS to True in tearDown when the class default is False, corrupting later tests through the base class, and test_spice_shape did not clear the Path registry, so a custom path ID was ignored whenever another test had already registered VENUS. tests/hosts is restored to its state on main, apart from import paths that still referred to oops.backplane.gold_master and to modules that moved out of oops/hosts into tests/hosts. Python 3.8 through 3.10 are no longer supported. requires-python and both CI matrices now begin at 3.11. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records the conventions and traps that are not evident from the code: the unittest-based test entry points, the flake8 targets and the deliberate whitespace ignores, the 80/90-column split between legacy and refactored modules, the banner-comment and trailing-underscore module conventions, and the environment variables the tests depend on. Also notes the architectural traps -- the late attribute injection at the foot of oops/__init__.py, masked polymath values, read-only cached Events and backplanes, the implicit km/sec-TDB/radian units, and quick=True disabling the optimization it appears to request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review available on request
Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
SpicePath.USE_SPICEPATH_SHORTCUTS was lost in the rewrite. It let a caller
force the general ancestry walk instead of the class-specific shortcut, which
is how a disagreement between the two gets localized.
The old flag cannot come back as it was: shortcuts are no longer specific to
SpicePath but are a generic _get_shortcut() hook, implemented by SpicePath,
SSBPath, SpiceFrame, SpiceType1Frame and J2000Frame. Path._USE_SHORTCUTS and
Frame._USE_SHORTCUTS therefore follow the existing _USE_QUICKPATHS pattern and
are consulted where _wrt() calls the hook. Both are read from the base class
rather than from the instance, so a subclass cannot shadow the switch and
leave part of the hierarchy still taking shortcuts.
The switch immediately finds one such disagreement, which is left for a
separate change: Path.as_path('SSB').wrt('EARTH', 'IAU_MARS') agrees with
cspyce.spkez to 1.5e-8 km through the SpicePath shortcut, but without it
RotatedPath rejects a frame whose center of rotation differs from the path's
origin. The SpicePath tests note this where they would otherwise loop over
both settings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RotatedPath refused to rotate a Path into a Frame whose center of rotation
differed from the Path's origin. The constraint does not hold: expressing a
state in rotating axes uses (d/dt)_rot A = (d/dt)_inertial A - omega x A,
which is valid for any vector A, so only the relative state being rotated
matters and not where the center of rotation lies.
The guard was added by the rewrite; main has no equivalent, and main ran
Path.as_path('SSB').wrt('EARTH', 'IAU_MARS') through the general ancestry walk
and agreed with cspyce.spkez. This branch only passed that test because the
SpicePath shortcut bypassed the guard.
With the guard gone, the general walk produces a RotatedPath agreeing with
cspyce.spkez to 3.0e-8 km in position and 6.4e-12 km/s in velocity over the
tested epochs, against test tolerances of 1e-7 and 1e-9.
The SpicePath tests now run twice again, once with shortcuts disabled and once
with them enabled, which is what the switch restored in the previous commit is
for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merging main brought in a Galileo call to SpiceType1Frame, written against
main's signature, into a branch that had rewritten that class. Nothing had
constructed a SpiceType1Frame on this branch before, so three faults surfaced
at once and the four Galileo gold-master tests failed.
* main's signature is (spice_frame, spice_host, tick_tolerance, ...); this
branch dropped spice_host, so SpiceType1Frame("GLL_SCAN_PLATFORM", -77, 40)
passed the spacecraft ID as the tick tolerance and the tolerance as the
reference frame. The two Voyager calls were stale in the same way. The host
argument is dropped from all three.
* The host is not lost by dropping it: _fill_spice_info already derives it as
cspyce.frinfo(frame_name)[0], which returns -77 for GLL_SCAN_PLATFORM and
-31/-32 for the Voyager scan platforms, matching the literals exactly. It is
now retained as _spice_origin_code rather than discarded as a local.
* SpiceType1Frame read _spice_body_code at fourteen sites and
_spice_origin_code at one, neither of which was ever assigned; both are the
spacecraft clock code that spice_host used to supply. They are unified on
_spice_origin_code.
* SpiceFrame._FOR_CODE does not exist; the attribute is _FOR_NAME, and the
surrounding call already keys it by frame name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
img.py and spe.py both opened with "from polymath import *" while referencing no polymath name at all, which is what flake8 reported as F401 alongside the F403 for the wildcard itself. Neither module uses eval, exec or getattr, and both are imported only as modules rather than for any name they might re-export, so the imports are simply deleted. This matches the treatment junocam received in #198. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The jiram package __init__ carried the same unused "from polymath import *" as img.py and spe.py. It references no polymath name, uses no eval, exec or getattr, and the only name imported from it elsewhere is JIRAM, which is defined in the file. No wildcard polymath import now remains anywhere under oops/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The matrix had been switched to ubuntu-latest, macos-latest and windows-latest. The tests cannot run there: scripts/automated_tests/ oops_main_test.sh sources ~/oops_runner_secrets and exits unless SPICE_PATH, SPICE_SQLITE_DB_NAME and OOPS_RESOURCES are set, none of which exist on a GitHub-hosted runner. Its "pip uninstall -y `pip freeze`" step also assumes a dedicated environment rather than a shared image. The job name also read "Test pdstemplate", which belongs to a different package. The cross-product form of the matrix is kept; it yields the same nine combinations as the previous explicit include list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four issue forms (bug report, feature request, other, plus a config that disables blank issues) and a pull request template whose sections are Purpose, Changes/Implementation Details, Type of Change, Testing, Potential Impacts, Checklist and Notes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The condition tested for ubuntu-latest, which has never appeared in this workflow's matrix, so the coverage report was never uploaded. It now matches self-hosted-linux on Python 3.13, which is one of the nine combinations. The path itself was already correct: oops_main_test.sh writes coverage.xml through "python -m coverage xml" once the suites have run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #203 +/- ##
==========================================
+ Coverage 75.40% 77.91% +2.51%
==========================================
Files 192 202 +10
Lines 24094 25198 +1104
Branches 2926 2784 -142
==========================================
+ Hits 18168 19633 +1465
+ Misses 5088 4707 -381
- Partials 838 858 +20 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Exercised this branch as a drop-in replacement for 1.
|
rfrenchseti
left a comment
There was a problem hiding this comment.
See earlier comment
|
Follow-up to my earlier comment: there is also a significant performance regression on this branch, concentrated in frames that aren't SymptomOn a geometry workload that evaluates ring surface intercepts across a full Cassini ISS frame, wall clock goes 25.1 s -> 53.5 s (2.1x). A second workload over the same image set that is dominated by FFT work outside oops is unchanged (33.69 s -> 33.81 s), and a mixed 75-image batch comes out at 1.30x overall — so this is not a flat per-call overhead, it is specific to certain frames. Where it goes
Instrumenting the size of the Proximate cause
if not frame._USE_QUICKFRAMES:
return frameAcross the whole tree only two classes opt in — On One caveat, since it is the obvious thing to trySetting Worth stressing that this is purely a performance issue — apart from the LORRI import break in my earlier comment, results on this branch match |
|
Regarding "2. Unregistered frames are retained for the life of the process", we can look into whether a strict upper limit on the Check out Until we get there, I think doing nothing is a reasonable approach to this issue, sinceit just wastes a bit of core memory, nothing more. If memory really is an issue, we could implement a workaround, such as limiting the size of the BTW, the |
|
"1. oops.hosts.newhorizons.lorri cannot be imported" is fixed in this checkin. "3. Two behavior/API changes that deserve a line in the PR description" is addressed by a few words in the PR description. The performance issue will take a little bit of investigation to determine whether the old or new behavior is correct. Specifically, what should happen is that the SpiceFrame uses a QuickFrame but the RingFrame should not. RingFrame + SpiceFrame should invisibly be implemented as RingFrame + QuickFrame, not as its own unique QuickFrame. |
|
Next point about timing. You should be using RingFrame(epoch=0.) for Saturn, because the rotation pole is (essentially) fixed. That means that the RingFrame, which is a "despun" version of the planet's PCK frame, is inertial. So by fixing the epoch at any time, you will be devoting roughly zero time to the evaluation of this frame. I see the timing issue you raised, and am looking into the best solution. But still, this is time you could be saving. |
The per-observation ownership documented for set_cmatrix(frame_id=None) is emergent from how the current registry treats unregistered frames, not a property Cmatrix or Frame.register promises. PR #203's registry rewrite dedups equal-valued unregistered frames to a shared wayframe and retains every construction globally, which would falsify the contract without anything failing. Pin it: equal-valued C-matrices yield distinct frame objects, and a default load leaves the wayframe registry and frame cache unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Wg9zMfz6FNEotebnS1hmm
|
Raising an interaction with #201, following @rfrenchseti's note there (#201 (comment)): as they describe it, the registry rewrite here inserts every frame into a per-subclass Two consequences for
#201 now pins its contract with a test ( |
Purpose
This branch introduces a Fittable/mutable framework for in-place parameter fitting, rewrites the Path and Frame registries around it, and fixes the regressions that rewrite introduced. It also drops Python 3.8 through 3.10.
The Fittable work is the motivation: pointing corrections, time shifts, plate scales and orbital elements need to be adjustable in place, with any object that depends on them refreshing itself when they change. Supporting that required reworking how Paths and Frames are registered, cached and linked.
Changes / Implementation Details
Fittable and mutable framework
oops/fittable.pyis rebuilt aroundset_params,refreshandfreeze. The newoops/mutable.pypropagates staleness through objects that merely contain a Fittable sub-object, tracking state through injected_FITTABLE*/_MUTABLE*attributes and an integer version counter.PathShift,FrameShift,TimeShiftandPlatescaleare new;OffsetFOV,Navigation,RotationandKeplerPathare converted. The four new classes are marked as untested placeholders.Path and Frame registry rewrite
Wayframe,Waypoint,AliasPath,AliasFrameandRelativeFrameare gone, replaced byNullFrame/J2000Frame,NullPath/SSBPath,ReversedFrame,RelativePath,ReversedPathandRotatedPath. Registries and the registration hook are private.QuickFrameandQuickPathmove into their own modules. A newoops/cache.pyprovides an LRU cache whoseclean_key()makes polymath objects usable as dictionary keys.Fixes to that rewrite
mutable._refresh_internalnever applied_refresh()on an object's first pass, so themutable.refresh(self)at the end of every constructor did nothing and derived attributes such as_transform,_timesand_pos_xwere never created.Frame._FRAME_CACHErather thanPath._PATH_CACHE, leaving the path cache unused and the frame cache holding Paths.Path._wrtlacked the reversal branch its Frame counterpart has, so linking a root path to one of its descendants recursed until the stack overflowed.RelativePathdiscarded the origin Path it needs for the subtraction, and advertised the frame of the wrong Path.LinkedFrame's origin check rejected a null frame origin, which is the ordinary case.QuickPath.for_pathandQuickFrame.for_frame, andextend()repaired in both -- a dict read as a method, frame keys used in a path class, a stale_steps, arrays whose lengths disagreed, a seam that left the tabulated times non-monotonic, and a tuple passed to a two-argument signature.SpicePath.get()ignoredoriginandframewhen asked for the SSB. SPICE accepts several names per body, so the name a caller uses is now registered alongside the canonical one;"GLL"resolves toGALILEO_ORBITER.KeplerPathhad eleven attribute names left behind by an incomplete privatization.os.path.basename()was applied to anFCPathin the Juno hosts, which is notos.PathLike; these now use theFCPath.nameproperty as the other hosts do.Python version
requires-pythonmoves to>=3.11and both CI matrices now begin at 3.11, testing 3.11 through 3.13 on Linux, macOS and Windows.Type of Change
Testing
All three entry points pass:
The main suite went from 19 failing tests to zero, and the host gold-master suite from four of five failing to zero. The Cassini ISS and Galileo SSI gold masters both compare clean, which exercises observation loading, backplane generation and comparison end to end.
Beyond the suite, the reversal path in
Path._wrtwas checked against independently computed geometry:earth.wrt(moon)matches both-moon.wrt(earth)and a separately constructedSpicePath('EARTH','MOON')to zero residual across 121 epochs.QuickPath.extendandQuickFrame.extendhave no test coverage at all, so they were driven directly and their interpolation checked against the underlying slow path and frame, agreeing to 4e-16 relative and 1e-15 absolute respectively.Several tests were themselves unsound and were fixed: the SpicePath tests restored
Path._USE_QUICKPATHStoTrueintearDownwhen the class default isFalse, corrupting later tests through the base class, andtest_spice_shapenever cleared the Path registry, so its custom path ID was ignored whenever an earlier test had already registered VENUS.Potential Impacts
Breaking, public API.
Wayframe,Waypoint,AliasPath,AliasFrameandRelativeFrameno longer exist.Path.as_path(id)now returns the registered Path rather than a zero-position Waypoint; useNullPathfor the old behaviour.AliasPath(path, frame)becomesNullPath(path, frame=frame). The registries are private (_PATH_REGISTRY,_FRAME_REGISTRY,_register(),_reset_caches()), the cross-class attributes injected byoops/__init__.pyare renamed fromXXX_CLASSto_Xxx,quickis keyword-only and defaults toNone, theQUICKdictionary keysquickpath_cache/quickframe_cachegain a_sizesuffix, automaticTEMPORARY_*path IDs are gone (path_idisNonewhen unregistered; useis_registered), andSpicePath.USE_SPICEPATH_SHORTCUTSno longer exists, so there is no longer a way to disable SPICE shortcuts for debugging.Python support. 3.8, 3.9 and 3.10 are dropped.
oops/cache.pyuses amatchstatement, so the floor cannot go back below 3.10 without rework.Performance.
Path._PATH_CACHEwas never being written to and so never hit; paths were rebuilt on every call. It now works as intended.Checklist
ruff check,ruff format) — n/a: this repository lints with flake8, not ruff; there is noruff.tomland no[tool.ruff]inpyproject.toml. Across the 109 modified.pyfiles, flake8 findings fall from 364 onmainto 310 here, so the change removes 54 and introduces none.mypypasses — n/a: no mypy configuration exists in the repository, and annotations are confined tofittable.pyandmutable.pyby design.Raises:clause ofRotatedPath. There is no Sphinx docs tree in this repository.printstatements dumping__dict__were removed fromtests/path/test_spicepath.py.Notes
Follow-up work, none of it blocking:
PathShift,FrameShift,PlatescaleandTimeShiftare labelledPLACEHOLDER CODE ... NOT YET TESTEDin their own docstrings.tests/hosts/juno/jiram/__init__.pyhas been migrated to the current gold-master API and theFCPathcrash fixed, so it can be enabled when wanted, but its April 2023 gold masters disagree with this branch on sky angles and ring geometry and would need triage first.run-lint.ymlhas itspull_request/pushtriggers commented out and pointing at amasterbranch that does not exist here, so flake8 has never run in CI.tests/hosts/unittester.pycovers two of the seven instrument packages;juno,hst,voyager,newhorizonsandkeckare all commented out.Gold-master directory rename
Gold masters are stored under a directory named after the module string, used verbatim as a path component (
oops/gold_master/__init__.py):The Juno masters were adopted in April 2023, before the host packages were renamed from
hosts.*tooops.hosts.*; the Cassini and Galileo masters were re-adopted in December 2023 and already use the new name. So the Juno tests ask foroops.hosts.juno.*and find nothing, reportingNo gold masterfor every backplane.The resource tree is not a git repository, so this has to be applied by hand wherever the resources live, including the self-hosted CI runners:
Afterwards all four directories share one convention:
The rename is reversible, and it is a no-op for anyone who does not run the Juno tests, since JunoCam and JIRAM are deliberately excluded from
tests/hosts/unittester.py. Note that once the masters are found, the JIRAM comparisons do run but disagree with this branch on sky angles and ring geometry, as noted above.🤖 Generated with Claude Code