Add custom C-matrix support via generic Observation.get_cmatrix/set_cmatrix - #201
Add custom C-matrix support via generic Observation.get_cmatrix/set_cmatrix#201jnspitale wants to merge 23 commits into
Conversation
Add cmatrix/frame_id/map_other_camera arguments to hosts/cassini/iss.py from_file, letting a caller supply a C-matrix (J2000 -> camera image frame) instead of SPICE pointing for the camera named in the label. Optionally derive and register the co-mounted other camera's frame from the fixed inter-camera rotation. The override lives in a new ISS.set_cmatrix method; the SPICE-derived camera frames are now built lazily by ISS.define_camera_frames() so a custom C-matrix without mapping never depends on SPICE (no CK loaded). Also add an override option to oops/frame/cmatrix.py Cmatrix so it can replace an already-registered frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generic getter on the Observation base class that evaluates self.frame's rotation relative to a reference frame (default J2000) at a time picked from a (u,v) location (default the FOV center), via the existing midtime_at_uv(). Returns the raw Matrix3 so it round-trips with the cmatrix inputs accepted by ISS.from_file()/ISS.set_cmatrix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWmXpTiygmBh786ED6GigN
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughChangesThe Cassini ISS host now accepts custom SPICE C-matrices, converts them into observation frames, and creates camera frames lazily. C-matrix registration supports controlled overrides. New tests cover conversions, frame isolation, CK loading, registration, and reset behavior. Cassini ISS C-matrix flow
Estimated code review effort: 4 (Complex) | ~45 minutes Mergeability Score: 🟡 Moderate · up to Repeated custom C-matrix assignment can leave stale camera pointing, potentially producing incorrectly oriented observations, while observations missing required host metadata fail unclearly. Merge should wait for explicit replacement handling and clearer validation. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@oops/hosts/cassini/iss.py`:
- Around line 497-501: The rel calculation in the camera-frame setup must use
immutable SPICE-only frame references rather than mutable CASSINI_ISS_* frames
that set_cmatrix can replace. Update define_camera_frames and the surrounding
frame setup to retain and use dedicated SPICE inter-camera frames for rel, and
add a regression test covering a single-camera override followed by a
mapped-camera load.
In `@tests/observation/test_snapshot.py`:
- Around line 135-139: Extend the snapshot observation test around cmatrix() so
it uses a time-dependent frame and observation whose midtime_at_uv() returns
different values for different UV coordinates. Assert that selecting distinct
UVs and times produces the corresponding transformed matrices, ensuring
cmatrix() actually uses both uv and time rather than returning the fixed
Cmatrix.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 05dd31e2-5483-41dc-b1c7-b30af5a8aac7
📒 Files selected for processing (4)
oops/frame/cmatrix.pyoops/hosts/cassini/iss.pyoops/observation/observation_.pytests/observation/test_snapshot.py
…ests Compute the map_other_camera inter-camera rotation from dedicated, immutable CASSINI_ISS_<camera>_SPICE frames instead of the mutable CASSINI_ISS_<camera> frames that set_cmatrix can override. A single-camera override followed by a mapped-camera load would otherwise derive `rel` from the overridden custom frame, corrupting the mapped camera's pointing. Add a regression test (test_map_after_single_camera_override) covering that ordering, wire the ISS Cmatrix test module into the unittester, and extend the Snapshot cmatrix() test to use a time-dependent SpinFrame and a uv-dependent midtime so it verifies cmatrix() actually consults both uv and time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
A recorded C-matrix is the SPICE camera-frame attitude, but the oops observation frame is that frame rotated 180 deg about the boresight, so feeding one into from_file(cmatrix=...) landed the frame ~0.5 deg off. Apply the fixed rotation (ROT180) at the boundary in ISS.set_cmatrix so a recorded/pxform C-matrix reproduces the observation's pointing, and add ISS.oops_from_host / host_from_oops to convert between the two conventions. Also fix two frame-registry defects: - A custom C-matrix no longer leaks into the next plain load. The default (frame_id=None) case now gives each observation its own unregistered Cmatrix frame instead of overriding the global CASSINI_ISS_<camera>, so loading another image never changes an earlier one's pointing. The map_other_camera case, which does override the globals, now clears ISS.frames_defined so the next plain load rebuilds them from SPICE. Correct the Observation.cmatrix() docstring: it returns the generic observation-frame attitude, which for ISS is not interchangeable with the SPICE-convention cmatrix input. Extend the ISS cmatrix tests: a pxform round-trip (zero offset), the default-frame isolation and no-leak regressions, the map reclaim path, and the oops_from_host / host_from_oops conversions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@oops/hosts/cassini/iss.py`:
- Around line 597-608: Update the mapped-camera handling around
define_camera_frames and the returned frame ID so each mapped observation
receives a separate fixed frame rather than the mutable global CASSINI_ISS
camera frame. Keep the global frames only for mapped-camera registration,
preserve the observation’s frame across subsequent plain and mapped loads, and
add a regression test that reads the first mapped observation after both a plain
load and a second mapped load.
In `@tests/hosts/cassini/iss/test_iss.py`:
- Around line 66-80: Use a consistent evaluation time for all C-matrix
comparisons in tests/hosts/cassini/iss/test_iss.py: in lines 66-80, compare
against baseline.cmatrix(time=baseline.tstart), and in lines 97-104, pass
time=baseline.tstart to both baseline.cmatrix() calls. Keep the existing
round-trip and boundary-rotation assertions unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 30a38d76-68e1-45c1-b1ae-bc8aad0c3fa1
📒 Files selected for processing (3)
oops/hosts/cassini/iss.pyoops/observation/observation_.pytests/hosts/cassini/iss/test_iss.py
Returns an ISS observation's pointing as a SPICE camera-frame C-matrix (as cspyce.pxform would return it), so it can be fed back into from_file(cmatrix=...) to reproduce the pointing. Implemented as host_from_oops(obs.cmatrix()), with uv/time selection delegated to Observation.cmatrix(). Adds a test covering the pxform match and the set/get round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the redundant rot180 = ROT180 local alias and reference the module-level ROT180 constant directly in the camera-frame constructions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@oops/hosts/cassini/iss.py`:
- Line 621: Add the return annotation -> oops.Matrix3 to ISS.get_cmatrix, or
explicitly exclude this method from ANN205 if this module intentionally omits
return annotations. Preserve the existing method behavior and signature
parameters.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: dee3c0fe-c3da-4dcc-bad5-8df0c442da67
📒 Files selected for processing (2)
oops/hosts/cassini/iss.pytests/hosts/cassini/iss/test_iss.py
Move the map_other_camera branch of set_cmatrix into its own method, ISS.set_cmatrix_both_cameras(cmatrix, camera, time), which registers both camera frames from one camera's custom C-matrix and returns the label camera's frame ID. set_cmatrix now delegates to it. Behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the cmatrix argument from ISS.map_other_camera; it now reads the label camera's current pointing from the registered CASSINI_ISS_<camera> frame and maps that to the other camera via the fixed inter-camera rotation. set_cmatrix's map_other_camera=True branch now installs the label camera's custom frame before delegating. Behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Observation.get_cmatrix() now derives the spice-frame (SPICE camera-frame) C-matrix at call time as host.CMATRIX_ROTATION * self.frame.wrt(J2000), with no stored cmatrix_frame subfield. The observation carries only a `host` reference; ISS exposes CMATRIX_ROTATION (renamed from ROT180) as a class attribute for this. - Remove the per-observation cmatrix_frame subfield from from_file/from_index. - Remove the redundant Observation.cmatrix() (raw oops-frame attitude accessor) and the host-side ISS.get_cmatrix(); get_cmatrix() is now the sole accessor. - Rename conversion helpers host_from_oops/oops_from_host -> spice_from_oops/oops_from_spice. - Rename ROT180 -> CMATRIX_ROTATION throughout. - Clarify docstrings around the two conventions: spice-frame (z=LOS, x left, y up) vs oops-frame (z=LOS, x right, y down), related by CMATRIX_ROTATION. - Use dedicated *_SPICE frames in define_camera_frames. - Repoint tests to get_cmatrix(); drop the removed cmatrix() tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors iss.set_cmatrix as the inverse of get_cmatrix, applying the host's CMATRIX_ROTATION to build an observation frame; supports an optional frame_id, without the map_other_camera machinery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Only the label camera's frame is ever created; a custom C-matrix never touches the other camera or the global frame registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observation.set_cmatrix now attaches the frame itself (assigns self.frame) instead of returning it. from_file builds the Snapshot with a placeholder J2000 frame when a custom C-matrix is given, then applies the pointing through the generic method once the host subfield is in place; no CKs are loaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@oops/observation/observation_.py`:
- Around line 482-487: Update get_cmatrix() and set_cmatrix() to explicitly
validate that the observation has a host subfield before accessing
host.CMATRIX_ROTATION. Raise a clear error identifying the missing convention
rotation when host is absent, while preserving the existing behavior when the
host is present.
- Around line 517-524: Update set_cmatrix to accept an override=False parameter
and replace the duplicated Cmatrix construction branches with one
Cmatrix(attitude, frame_id=frame_id, override=override) call. Document that
override=True replaces the existing primary definition for a duplicate frame_id,
while preserving unregistered-frame behavior when frame_id is None.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1da334a4-806c-418f-9170-cb7f3b10316d
📒 Files selected for processing (3)
oops/hosts/cassini/iss.pyoops/observation/observation_.pytests/hosts/cassini/iss/test_iss.py
… frame_id reuse, informative errors get_cmatrix/set_cmatrix now derive the spice-frame<->oops-frame inverse rotation by transposing the host's single CMATRIX_ROTATION attribute, instead of requiring hosts to expose a redundant CMATRIX_ROTATION_INV that could be defined inconsistently. Re-using a frame_id in set_cmatrix now passes override=True so the new pointing cleanly replaces the primary frame definition instead of leaving a stale one behind (Frame.register's secondary-definition path). Both methods now raise a clear ValueError when the observation's host doesn't define CMATRIX_ROTATION, rather than a bare AttributeError. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWmXpTiygmBh786ED6GigN
frame_id was only consulted when cmatrix was also given; passing it alone was silently ignored rather than raising. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWmXpTiygmBh786ED6GigN
Cassini.used_kernels() gained a ck keyword; from_file passes ck=(cmatrix is None). Without this, a custom-cmatrix observation's spice_kernels could list CK basenames it never used -- e.g. the gapfill CKs Cassini.initialize() furnishes unconditionally, or a CK furnished earlier in the session by a plain SPICE-pointed load. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWmXpTiygmBh786ED6GigN
- "coordinate system" had a double space. - The define_camera_frames comment claimed override=True refreshes frames after ISS.reset(), but neither ISS.reset() nor Cassini.reset() clears the frame registry, so the *_SPICE SpiceFrames (registered without override) leave a stale primary definition after a rebuild. Harmless here since they're used as direct object references, not looked up by ID, but the comment now says so instead of implying a full refresh. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWmXpTiygmBh786ED6GigN
|
One forward-compatibility note on The But it isn't a property that If #203 lands after this, the docstring's guarantee becomes false without anything in either PR failing — the frame is no longer owned by the observation, and it is no longer isolated from global state. Two observations handed equal-valued C-matrices would share a frame object. Either of these would be enough to keep that from happening quietly:
Worth coordinating with #203 either way, since the two PRs are independent branches off |
rfrenchseti
left a comment
There was a problem hiding this comment.
See earlier comments
With offset_wac=True, the WAC observation frame carried a Navigation offset on top of the SPICE chain, so get_cmatrix() reported a composite attitude rather than the pxform value its docstring promises, and the meaning of from_file(cmatrix=) depended on a flag set at initialize time. Resolves finding F3 of the rms-oops #201 verification review by removing the option entirely: both camera frames are now built the same way, directly from the SPICE frame and CMATRIX_ROTATION. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Wg9zMfz6FNEotebnS1hmm
Address findings F1, F2, F5 and F6 of the rms-oops #201 verification review: - F1: set_cmatrix() now rejects anything that is not a proper floating-point rotation -- non-finite values, boolean/integer data, reflections, scalings and non-orthogonal matrices all raise ValueError at the boundary instead of corrupting geometry downstream. - F2: set_cmatrix(frame_id=) only overrides a registration it made itself; a frame_id held by any other frame (e.g. CASSINI_ISS_NAC or J2000) raises instead of silently replacing its primary definition. Re-using a set_cmatrix-issued ID still re-points every observation sharing that frame, as documented. - F5: document that geometry derived before a set_cmatrix() call is not invalidated and must be rebuilt. - F6: document that a custom-cmatrix load furnishes no CK, so pxform against the SPICE camera frame at that epoch will fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Wg9zMfz6FNEotebnS1hmm
Move the proper-rotation checks out of set_cmatrix() into a standalone static method that returns the validated Matrix3, so callers (e.g. a host's from_file) can validate a recorded C-matrix at any boundary without mutating an observation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Wg9zMfz6FNEotebnS1hmm
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
|
Good catch — agreed that the isolation property was emergent rather than promised anywhere. Addressed by pinning it as a test rather than weakening the docstring, since per-observation ownership is the feature this argument exists for, not an accident we're willing to lose:
The retention/dedup question itself is #203's design call — raised there with a pointer back to this contract. |
|
This is ready for re-review. Main concern is interaction with 203. |
markshowalter
left a comment
There was a problem hiding this comment.
We're going to need to discuss this. My first issue is that the override option in Cmatrix needs to be implemented via the new Fittable/Mutable API. You can't just replace the definition of an object without notifying any object on which it depends. This is what the new API handles automatically. I did not define class Cmatrix to be Fittable, and recommend that we don't. Instead, a user should be able to define a Navigation frame that operates on the Cmatrix, and then adjust the Navigation. A lot of this needs to be built into the planned rewrite of the host modules. Until then, we can discuss what is specifically needed as an interim fix.
Maybe we can just define every Cassini frame as a Navigation atop a Cmatrix, because a Navigation object has very little overhead, and then you could update the Navigation at will using obs.frame.set_params(). That would guarantee that the changes propagate forward to anything derived from obs, such as backplanes. However, the quantities you use to define the Navigation are 2 or 3 rotation angles, not a brand new Cmatrix.
My only issue with set_cmatrix is the name, because we use "cmatrix" widely in the source code to refer to the OOPS version, whereas this function wants to return the SPICE C kernel definition, whatever it is. The solution would be to rename it get_spice_cmatrix, so there is no ambiguity. Eventually, get_spice_cmatrix will need to be implemented for all hosts, but that can come with the hosts cleanup.
|
I also do not like using cmatrix when we're talking about an orientation in the OOPS convention. I use "orientation matrix" to denote this in OMINAS and reserve "cmatrix" for the SPICE orientation matrix. This would be less confusing. |
|
I'm assuming this should be closed now since it's been superseded by #203. |
Summary
hosts/cassini/iss.pyfrom_file()accepts a caller-suppliedcmatrix=— the spice-convention C-matrix (J2000 →CASSINI_ISS_<camera>, exactly as returned bycspyce.pxform()or recorded in a CK) — plus an optionalframe_id=. A custom-cmatrix load never loads a CK, never builds the SPICE camera frames (now built lazily byISS.define_camera_frames()), and never reports CKs inspice_kernels.Observation.get_cmatrix(uv=None, time=None)andObservation.set_cmatrix(cmatrix, frame_id=None)on the base class. Both are driven by a single host class attribute,CMATRIX_ROTATION(the spice-frame → oops-frame convention rotation, reached through the observation'shostsubfield); the inverse direction is derived by transposition. No host-specific set/get methods, and nothing frame-shaped is stored on the observation —get_cmatrixderives the C-matrix fromself.frameat call time.set_cmatrixvalidates its input: the matrix must be a proper floating-point rotation. Non-finite values, boolean/integer data, reflections, scalings, and non-orthogonal matrices raiseValueErrorat the boundary.set_cmatrix(frame_id=)only overrides frame registrations it made itself; an ID already held by any other frame (e.g.CASSINI_ISS_NAC,J2000) raises instead of silently replacing its primary definition.frame_id=None(default) gives each observation its own unregistered frame; re-using a set_cmatrix-issued ID re-points every observation sharing that registered frame, which is the point of giving it an ID.oops/frame/cmatrix.pyCmatrixgains anoverride=option so an owned registration can be cleanly replaced.Breaking changes
CASSINI_ISS_NAC_FLIPPED/CASSINI_ISS_WAC_FLIPPEDare renamed to*_SPICE.ISS.initialize()alone no longer registersCASSINI_ISS_NAC/CASSINI_ISS_WAC; they appear after the first plain (SPICE-pointed)from_file(), which callsdefine_camera_frames().offset_wacinitialize option is removed: it made the WAC pointing a composite (Navigation offset on top of the SPICE chain), soget_cmatrixcould not agree withpxformand the meaning offrom_file(cmatrix=)depended on an initialize-time flag.Review
Addresses the independent verification review (Custom C-Matrix Verification, R. French, 2026-08-14): F1 rotation validation, F2 frame-registry protection, F3
offset_wacremoval, F5/F6 docstring clarifications (stale derived geometry; no CK loaded sopxformagainst the SPICE camera frame fails for that epoch). F4 (enabling the other hosts: Galileo SSI, Voyager ISS, NH LORRI) is deliberately deferred to #204 — this branch is the template.Test plan
tests/hosts/cassini/iss/test_iss.py— 18 tests: round-trip vspxform, per-observation frame isolation, no leakage into plain loads, frame_id reuse/collision, C-matrix validation, CK loading andspice_kernelsreportingtests/frame/test_cmatrix.pytests/observationsuite🤖 Generated with Claude Code
https://claude.ai/code/session_015Wg9zMfz6FNEotebnS1hmm