Skip to content

Commit 278b965

Browse files
committed
Add NDCube._extra_attrs_to_copy for subclass attribute propagation
Subclasses can declare instance attributes that are automatically propagated (by reference) to cubes derived through _new_instance (arithmetic operations) and through to_nddata when the target type is a subclass carrying the same attributes. This removes the need for subclasses to override both methods just to keep domain-specific metadata alive on derived cubes. Slicing is deliberately not covered, as shape-dependent attributes cannot be sliced generically.
1 parent 685c1f7 commit 278b965

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

changelog/940.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added the ``NDCube._extra_attrs_to_copy`` extension point. Subclasses can declare instance attributes that are automatically propagated to cubes derived through arithmetic operations (``_new_instance``) and through ``to_nddata`` when the target type is a subclass carrying the same attributes.

ndcube/ndcube.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,15 @@ class NDCubeBase(NDCubeABC, astropy.nddata.NDData, NDCubeSlicingMixin):
378378
_extra_coords = NDCubeLinkedDescriptor(ExtraCoords)
379379
_global_coords = NDCubeLinkedDescriptor(GlobalCoords)
380380

381+
# Names of additional instance attributes which subclasses want propagated
382+
# (by reference) to instances derived through `_new_instance` (e.g.
383+
# arithmetic operations) and `to_nddata` when the target type carries the
384+
# same attributes. Subclasses should override this with a tuple of
385+
# attribute names. Note these attributes are not automatically modified
386+
# when a cube is sliced; subclasses with shape-dependent attributes must
387+
# handle slicing themselves.
388+
_extra_attrs_to_copy = ()
389+
381390
def __init__(self, data, wcs=None, uncertainty=None, mask=None, meta=None,
382391
unit=None, copy=False, psf=None, *, extra_coords=None, global_coords=None, **kwargs):
383392

@@ -963,6 +972,9 @@ def _new_instance(self, **kwargs):
963972
new_cube._extra_coords = deepcopy(self.extra_coords)
964973
if self.global_coords is not None:
965974
new_cube._global_coords = deepcopy(self.global_coords)
975+
for attr in self._extra_attrs_to_copy:
976+
if hasattr(self, attr):
977+
setattr(new_cube, attr, getattr(self, attr))
966978
return new_cube
967979

968980
def __neg__(self):
@@ -1635,6 +1647,12 @@ def to_nddata(self,
16351647
array([[1., 1., 1.],
16361648
[1., 1., 1.]])
16371649
"""
1650+
# If the target type carries the same subclass-specific attributes as
1651+
# this cube, copy them by default unless explicitly overridden.
1652+
if isinstance(nddata_type, type) and issubclass(nddata_type, type(self)):
1653+
for attr in self._extra_attrs_to_copy:
1654+
if hasattr(self, attr):
1655+
kwargs.setdefault(attr, "copy")
16381656
# Put all NDData kwargs in a dict
16391657
user_kwargs = {"data": data,
16401658
"wcs": wcs,

ndcube/tests/test_ndcube.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,47 @@ def __init__(self, data, *, spam=None, **kwargs):
293293
assert new_ndd.spam == "Eggs"
294294
assert new_ndd.data is ndc.data
295295
assert new_ndd.wcs is ndc.wcs
296+
297+
298+
class SidecarCube(NDCube):
299+
"""NDCube subclass declaring extra attributes to propagate to derived cubes."""
300+
301+
_extra_attrs_to_copy = ("observer", "calibration_level")
302+
303+
def __init__(self, *args, **kwargs):
304+
self.observer = kwargs.pop("observer", None)
305+
self.calibration_level = kwargs.pop("calibration_level", 0)
306+
super().__init__(*args, **kwargs)
307+
308+
309+
@pytest.fixture
310+
def sidecar_cube(wcs_3d_lt_ln_l):
311+
cube = SidecarCube(np.ones((2, 3, 4)), wcs=wcs_3d_lt_ln_l)
312+
cube.observer = "earth"
313+
cube.calibration_level = 2
314+
return cube
315+
316+
317+
def test_extra_attrs_to_copy_propagate_through_arithmetic(sidecar_cube):
318+
doubled = sidecar_cube * 2
319+
assert type(doubled) is SidecarCube
320+
assert doubled.observer == "earth"
321+
assert doubled.calibration_level == 2
322+
323+
negated = -sidecar_cube
324+
assert negated.observer == "earth"
325+
assert negated.calibration_level == 2
326+
327+
328+
def test_extra_attrs_to_copy_propagate_through_to_nddata(sidecar_cube):
329+
copied = sidecar_cube.to_nddata(nddata_type=SidecarCube)
330+
assert copied.observer == "earth"
331+
assert copied.calibration_level == 2
332+
333+
# Explicit kwargs override the automatic copy.
334+
overridden = sidecar_cube.to_nddata(nddata_type=SidecarCube, observer="sdo")
335+
assert overridden.observer == "sdo"
336+
337+
# Types which do not carry the attributes are unaffected.
338+
plain = sidecar_cube.to_nddata(nddata_type=astropy.nddata.NDData)
339+
assert not hasattr(plain, "observer")

0 commit comments

Comments
 (0)