astropy / astropy/ccdproc

subtract_dark(scale=True) promotes float32 inputs to float64; scale=False does not

Open Beginner friendly
#1,013 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
93
Forks
92
Avg merge
14h 44m
Merged PRs (30d)
30

Description

This issue was written and opened by Claude Code at the direction of Matt Craig (@mwcraig).

Environment: ccdproc 2.4.3, astropy 7.1.0, numpy 2.3.5.

With a float32 image and a float32 master dark, `subtract_dark(ccd, master, scale=True)`
returns float64 data; `scale=False` returns float32. Cause (`ccdproc/core.py`): the
`scale=True` branch does
```python
master_scaled = master.copy() # line 722
master_scaled = master_scaled.multiply(data_exposure / dark_exposure) # line 725
```
`data_exposure / dark_exposure` is a 0-d `astropy.units.Quantity`, treated as float64 rather
than a weak scalar; multiplying the float32 master by it upcasts to float64, so
`ccd.subtract(master_scaled)` then returns float64 even though `ccd` is float32. The
`scale=False` branch calls `ccd.subtract(master)` directly with no intermediate multiply, so it
stays float32. The `master.copy()` on line 722 is also unnecessary: `multiply()` already
returns a new object.

Reproducer and output:
```python
import numpy as np
from astropy.nddata import CCDData
import astropy.units as u
import ccdproc

rng = np.random.default_rng(0)
light = CCDData(rng.random((50, 50)).astype(np.float32), unit=u.adu, meta={'exposure': 30.0})
dark = CCDData(rng.random((50, 50)).astype(np.float32) * 0.1, unit=u.adu, meta={'exposure': 10.0})

r_scaled = ccdproc.subtract_dark(light, dark, exposure_time='exposure',
exposure_unit=u.second, scale=True)
r_unscaled = ccdproc.subtract_dark(light, dark, exposure_time='exposure',
exposure_unit=u.second, scale=False)
print(r_scaled.data.dtype, r_unscaled.data.dtype)
```
Output: `float64 float32`

Suggested fix: cast the ratio to `master.dtype` (or use a plain Python `float`) before
multiplying, e.g. `master_scaled.multiply(np.array(data_exposure / dark_exposure,
dtype=master.dtype))`, and drop the `.copy()` on line 722.

Cost in practice: on a 4096x4096 float32 light with a scaled dark, this promotion (extra copy
plus float64 intermediate) measured 336 MB peak vs 201 MB when the ratio is pre-cast to float32
before scaling.

Contributor guide

Open the contributing guide

Research direction

Read ccdproc/core.py around lines 722 and 725, then run the reproducer with float32 light and dark CCDData. The fix is complete when scale=True preserves the float32 result like scale=False, removes the unnecessary copy, and avoids the float64 intermediate and its extra memory use.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python
Domain
data, performance
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.