# Diagnose and fix astropy Quantity and unit errors

Diagnose an astropy.units failure by exception class: UnitConversionError (incompatible physical types vs missing equivalencies like spectral/doppler/dimensionless_angles), TypeError on float()/int() of unit-carrying quantities, and silent unit loss at the numpy boundary (np.array/np.broadcast_to without subok=True). Prescribes equivalencies, to_value(), and subclass-preserving calls.

Exact reference: {"kind":"skill_version","skill_id":"skl_i7aE0oAgt7KHRvp7F6jQaw","version_id":"skv_fDCCsg8nwLQn2pOwEhE7RQ"}

Applicability: [{"constraint":">=4.0","technology":"astropy","version_scheme":"semver"}]

# Diagnose and fix astropy Quantity and unit errors

Use this when arithmetic with `astropy.units` raises `UnitConversionError` or
`TypeError`, or when units silently vanish and downstream numbers come out
wrong by orders of magnitude. Nearly every unit bug is one of four failure
modes, and this procedure identifies which one from the exception (or its
absence) before you touch the code.

## 1. Read the exception class — it names the failure mode

- `UnitConversionError: 'm' (length) and 's' (time) are not convertible` —
  the two operands have genuinely incompatible physical types, or they are
  convertible only through an equivalency you have not enabled (section 2).
- `TypeError: only dimensionless scalar quantities can be converted to
  Python scalars` — you called `float()`, `int()`, or `round()` on a
  quantity that still carries a physical unit (section 3).
- No exception, but later code misbehaves and values look like bare numbers
  — the units were silently dropped, almost always by a numpy function that
  was not told to preserve subclasses (section 4).

Check the physical types of both sides first — it is the fastest diagnostic:

```python
from astropy import units as u

q = 500 * u.nm
print(q.physical_type)                 # 'length'
print(q.is_equivalent(u.Hz))           # False -- needs an equivalency
print((10 * u.m).is_equivalent(u.km))  # True -- plain conversion, no equivalency needed
```

## 2. UnitConversionError: incompatible types vs missing equivalency

`UnitConversionError` from `.to()` or from `+`/`-` means one of two things.
Distinguish them with `is_equivalent()`:

- `is_equivalent(target)` is `True` — the conversion is legal but something
  else is wrong (usually a plain number was passed where a `Quantity` was
  expected). Convert explicitly with `.to()`.
- `is_equivalent(target)` is `False` — either the conversion is genuinely
  meaningless (length to time: your formula is wrong, fix the math), or it
  needs an **equivalency**: a context-dependent mapping between physical
  types.

The equivalencies you will actually reach for:

```python
freq = (500 * u.nm).to(u.Hz, equivalencies=u.spectral())  # wavelength <-> frequency <-> energy
```

- `u.spectral()` — wavelength, frequency, energy. It does *not* cover
  Doppler shifts; that is a separate equivalency, a common confusion.
- `u.doppler_optical(rest_wavelength)` / `u.doppler_radio(...)` —
  wavelength/frequency <-> velocity, optical and radio conventions.
- `u.spectral_density(ref_wavelength)` — flux density <-> surface
  brightness, needs a reference wavelength.
- `u.dimensionless_angles()` — angle <-> dimensionless (radians to 1).
- `u.mass_energy()`, `u.brightness_temperature()` — niche but real.

Scope the equivalency to the block that needs it rather than enabling it
globally:

```python
with u.set_enabled_equivalencies(u.dimensionless_angles()):
    x = (0.5 * u.rad).to(u.dimensionless_unscaled)   # 0.5
```

## 3. TypeError on float()/int(): convert to a plain number deliberately

`float(3 * u.m)` always fails. The fix is to say which unit the number is
in, explicitly:

```python
float((3.2 * u.km).to(u.m))    # 3200.0
(3.2 * u.km).to_value(u.m)     # 3200.0 -- plain float, no intermediate Quantity
```

`to_value(unit)` is the right API when you want a number out; `.value`
alone returns the number in whatever unit the quantity currently carries,
which is often not the unit you assumed.

Dimensionless quantities are the exception — `float()` works and the scale
is folded in (`float(3. * u.km / (4. * u.m))` is `750.0`). But watch the
subtlety: a "dimensionless" quantity like `1. * u.m / u.km` has unit `m/km`
with a scale of 0.001. Adding it to a plain float auto-decomposes to
scale-free (`1. + 1. * u.m / u.km` gives `1.001`), while `.value` gives the
raw `1.0`. If you want the scaled number, call `.decompose()` first or
convert explicitly.

## 4. Units silently dropped: the numpy boundary

`Quantity` is an ndarray subclass, and several numpy entry points return a
bare ndarray unless asked to preserve the subclass:

```python
q = np.arange(10.) * u.m
np.array(q)                          # bare array -- units GONE (documented)
np.array(q, subok=True)              # keeps the Quantity, units of m
np.broadcast_to(q, (2, 10))          # bare array -- units GONE
np.broadcast_to(q, (2, 10), subok=True)   # keeps units
```

Same class of bug: calling `.value` "just to be safe" and then continuing
in unit-less code, or feeding a quantity to a library function that
internally calls `np.asarray` without `subok=True`. When a downstream
result is mysteriously off by orders of magnitude, check `type(result)` and
`result.unit` at the numpy boundary before doubting the physics.

For validating function arguments, `u.quantity_input` checks that inputs
are quantities with units convertible to the declared one — it checks, it
does *not* convert, so `myfunction(100 * u.arcsec)` still arrives as
arcseconds when the declared unit is degrees.

## 5. Checklist

1. Read the exception class: `UnitConversionError` / scalar-conversion
   `TypeError` / silent (no exception).
2. `UnitConversionError` → run `is_equivalent()` on both sides. `True`:
   wrong operand shape, convert explicitly. `False`: fix the formula or
   add an equivalency.
3. Equivalency needed → pass `equivalencies=` to `.to()`, or
   `set_enabled_equivalencies` for a block. `spectral()` does not cover
   Doppler.
4. `TypeError` on `float`/`int` → `.to(unit).value` or `.to_value(unit)`;
   never `float(q)` directly on a unit-carrying quantity.
5. Silent loss → grep the numpy boundary for `np.array(`, `np.broadcast_to`,
   `np.asarray` without `subok=True`; check `.value` is not taken too early.


## Supporting basis and limitations

Built from the astropy Quantity documentation (converting to plain Python scalars, dimensionless quantities, quantity_input), the units equivalencies documentation, and astropy's known-issues notes on quantities losing units through np.array/np.broadcast_to and on UnitConversionError from dimensionless arithmetic (astropy issues #7582, #7832).

## Change and rationale

New skill: diagnose and fix astropy Quantity and unit errors.

Unit errors are among the most-asked astropy questions, and the failure mode determines the fix: an equivalency problem needs u.spectral(), a float(q) TypeError needs to_value(), and silent unit loss needs subok=True. Without classifying first, agents guess at conversions and introduce order-of-magnitude bugs.
