diff --git a/astropy/units/quantity.py b/astropy/units/quantity.py index b4ec70f35b8..febf507b76a 100644 --- a/astropy/units/quantity.py +++ b/astropy/units/quantity.py @@ -1687,8 +1687,9 @@ def _to_own_unit(self, value, check_precision=True, *, unit=None): # Setting names to ensure things like equality work (note that # above will have failed already if units did not match). - if self.dtype.names: - _value.dtype.names = self.dtype.names + # TODO: is this the best place to do this? + if _value.dtype.names is not None: + _value = _value.astype(self.dtype, copy=False) return _value def itemset(self, *args): diff --git a/astropy/units/structured.py b/astropy/units/structured.py index 41cf4c6e920..72d89768433 100644 --- a/astropy/units/structured.py +++ b/astropy/units/structured.py @@ -22,7 +22,7 @@ def _names_from_dtype(dtype): """Recursively extract field names from a dtype.""" names = [] for name in dtype.names: - subdtype = dtype.fields[name][0] + subdtype = dtype.fields[name][0].base if subdtype.names: names.append([name, _names_from_dtype(subdtype)]) else: diff --git a/astropy/units/tests/test_quantity_array_methods.py b/astropy/units/tests/test_quantity_array_methods.py index db1979e154c..32e74b15bbc 100644 --- a/astropy/units/tests/test_quantity_array_methods.py +++ b/astropy/units/tests/test_quantity_array_methods.py @@ -610,9 +610,13 @@ def test_not_implemented(self): q1.dumps() -class TestRecArray: - """Record arrays are not specifically supported, but we should not - prevent their use unnecessarily""" +class TestStructuredArray: + """Structured arrays are not specifically supported, but we should not + prevent their use unnecessarily. + + Note that these tests use simple units. Now that structured units are + supported, it may make sense to deprecate this. + """ def setup_method(self): self.ra = ( @@ -627,3 +631,18 @@ def test_equality(self): qra = u.Quantity(self.ra, u.m) qra[1] = qra[2] assert qra[1] == qra[2] + + def test_assignment_with_non_structured(self): + qra = u.Quantity(self.ra, u.m) + qra[1] = 0 + assert qra[1] == np.zeros(3).view(qra.dtype) + + def test_assignment_with_different_names(self): + qra = u.Quantity(self.ra, u.m) + dtype = np.dtype([("x", "f8"), ("y", "f8"), ("z", "f8")]) + value = np.array((-1.0, -2.0, -3.0), dtype) << u.km + qra[1] = value + assert qra[1] == value + assert qra[1].value == np.array((-1000.0, -2000.0, -3000.0), qra.dtype) + # Ensure we do not override dtype names of value. + assert value.dtype.names == ("x", "y", "z") diff --git a/astropy/units/tests/test_structured.py b/astropy/units/tests/test_structured.py index 5a66abd5e9d..84269c8b508 100644 --- a/astropy/units/tests/test_structured.py +++ b/astropy/units/tests/test_structured.py @@ -109,6 +109,21 @@ def test_extreme_recursive_initialization(self): 'l', ) # fmt: skip + dt = np.dtype( + [("t", "f8"), + ("pvhd1d2", + ([("p", "f8"), ("v", "f8"), ("hd1d2", + [("h", "f8"), ("d1d2", + [("d1", "f8"), ("d2", "f8")]), + ]), + ], (5, 5))), # Note: structured subarray to improve test! + ("l", "f8") + ]) # fmt: skip + + su2 = StructuredUnit("(yr,(AU,AU/day,(km,(day,day))),m)", dt) + assert su2.field_names == su.field_names + assert su2 == su + @pytest.mark.parametrize( "names, invalid", [ @@ -122,7 +137,7 @@ def test_extreme_recursive_initialization(self): ) def test_initialization_names_invalid_list_errors(self, names, invalid): with pytest.raises(ValueError) as exc: - StructuredUnit("(yr,(AU,AU/day)", names) + StructuredUnit("yr,(AU,AU/day)", names) assert f"invalid entry {invalid}" in str(exc) def test_looks_like_unit(self): diff --git a/docs/changes/units/14680.bugfix.rst b/docs/changes/units/14680.bugfix.rst new file mode 100644 index 00000000000..16ac0dba816 --- /dev/null +++ b/docs/changes/units/14680.bugfix.rst @@ -0,0 +1,4 @@ +Ensure that ``Quantity`` with structured dtype can be set using non-structured +``Quantity`` (if units match), and that structured dtype names are inferred +correctly in the creation of ``StructuredUnit``, thus avoiding mismatches +when setting units.