drizzle 2.2.0__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2202 @@
1
+ import math
2
+ import os
3
+ from itertools import product
4
+
5
+ import numpy as np
6
+ import pytest
7
+
8
+ from astropy import wcs
9
+ from astropy.convolution import Gaussian2DKernel
10
+ from drizzle import cdrizzle, resample, utils
11
+
12
+ from .helpers import wcs_from_file
13
+
14
+ TEST_DIR = os.path.abspath(os.path.dirname(__file__))
15
+ DATA_DIR = os.path.join(TEST_DIR, "data")
16
+
17
+
18
+ def bound_image(image):
19
+ """
20
+ Compute region where image is non-zero
21
+ """
22
+ coords = np.nonzero(image)
23
+ ymin = coords[0].min()
24
+ ymax = coords[0].max()
25
+ xmin = coords[1].min()
26
+ xmax = coords[1].max()
27
+ return (ymin, ymax, xmin, xmax)
28
+
29
+
30
+ def centroid(image, size, center):
31
+ """
32
+ Compute the centroid of a rectangular area
33
+ """
34
+ ylo = int(center[0] - size / 2)
35
+ yhi = min(ylo + size, image.shape[0])
36
+ xlo = int(center[1] - size / 2)
37
+ xhi = min(xlo + size, image.shape[1])
38
+
39
+ yx1 = np.mgrid[ylo:yhi, xlo:xhi, 1:2]
40
+ center = (yx1[..., 0] * image[ylo:yhi, xlo:xhi]).sum(
41
+ axis=(1, 2),
42
+ dtype=np.float64,
43
+ )
44
+
45
+ if center[2] == 0.0:
46
+ return None
47
+
48
+ center[0] /= center[2]
49
+ center[1] /= center[2]
50
+ return center
51
+
52
+
53
+ def centroid_close(list_of_centroids, size, point):
54
+ """
55
+ Find if any centroid is close to a point
56
+ """
57
+ for i in range(len(list_of_centroids) - 1, -1, -1):
58
+ if abs(list_of_centroids[i][0] - point[0]) < int(size / 2) and abs(
59
+ list_of_centroids[i][1] - point[1]
60
+ ) < int(size / 2):
61
+ return 1
62
+
63
+ return 0
64
+
65
+
66
+ def centroid_compare(centroid):
67
+ return centroid[1]
68
+
69
+
70
+ def centroid_distances(image1, image2, amp, size):
71
+ """
72
+ Compute a list of centroids and the distances between them in two images
73
+ """
74
+ distances = []
75
+ list_of_centroids = centroid_list(image2, amp, size)
76
+ for cc in list_of_centroids:
77
+ center1 = centroid(image1, size, cc)
78
+ center2 = centroid(image2, size, cc)
79
+ if center1 is None:
80
+ continue
81
+
82
+ disty = center2[0] - center1[0]
83
+ distx = center2[1] - center1[1]
84
+ dist = math.sqrt(disty * disty + distx * distx)
85
+ dflux = abs(center2[2] - center1[2])
86
+ distances.append([dist, dflux, center1, center2])
87
+
88
+ distances.sort(key=centroid_compare)
89
+ return distances
90
+
91
+
92
+ def centroid_list(image, amp, size):
93
+ """
94
+ Find the next centroid
95
+ """
96
+ list_of_centroids = []
97
+ points = np.transpose(np.nonzero(image > amp))
98
+ for point in points:
99
+ if not centroid_close(list_of_centroids, size, point):
100
+ center = centroid(image, size, point)
101
+ list_of_centroids.append(center)
102
+
103
+ return list_of_centroids
104
+
105
+
106
+ def centroid_statistics(title, fname, image1, image2, amp, size):
107
+ """
108
+ write centroid statistics to compare differences btw two images
109
+ """
110
+ stats = ("minimum", "median", "maximum")
111
+ images = (None, None, image1, image2)
112
+ im_type = ("", "", "test", "reference")
113
+
114
+ diff = []
115
+ distances = centroid_distances(image1, image2, amp, size)
116
+ indexes = (0, int(len(distances) / 2), len(distances) - 1)
117
+ fd = open(fname, "w")
118
+ fd.write(f"*** {title:s} ***\n")
119
+
120
+ if len(distances) == 0:
121
+ diff = [0.0, 0.0, 0.0]
122
+ fd.write("No matches!!\n")
123
+
124
+ elif len(distances) == 1:
125
+ diff = [distances[0][0], distances[0][0], distances[0][0]]
126
+
127
+ fd.write("1 match\n")
128
+ fd.write(f"distance = {distances[0][0]:f} flux difference = {distances[0][1]:f}\n")
129
+
130
+ for j in range(2, 4):
131
+ ylo = int(distances[0][j][0]) - 1
132
+ yhi = int(distances[0][j][0]) + 2
133
+ xlo = int(distances[0][j][1]) - 1
134
+ xhi = int(distances[0][j][1]) + 2
135
+ subimage = images[j][ylo:yhi, xlo:xhi]
136
+ fd.write(
137
+ f"\n{im_type[j]} image centroid = "
138
+ f"({distances[0][j][0]:f}, {distances[0][j][1]:f}) "
139
+ f"image flux = {distances[0][j][2]:f}\n"
140
+ )
141
+ fd.write(str(subimage) + "\n")
142
+
143
+ else:
144
+ fd.write(f"{len(distances)} matches\n")
145
+
146
+ for k in range(3):
147
+ i = indexes[k]
148
+ diff.append(distances[i][0])
149
+ fd.write(
150
+ f"\n{stats[k]} distance = {distances[i][0]:f} "
151
+ f"flux difference = {distances[i][1]:f}\n"
152
+ )
153
+
154
+ for j in range(2, 4):
155
+ ylo = int(distances[i][j][0]) - 1
156
+ yhi = int(distances[i][j][0]) + 2
157
+ xlo = int(distances[i][j][1]) - 1
158
+ xhi = int(distances[i][j][1]) + 2
159
+ subimage = images[j][ylo:yhi, xlo:xhi]
160
+ fd.write(
161
+ f"\n{stats[k]} {im_type[j]} image centroid = "
162
+ f"({distances[i][j][0]:f}, {distances[i][j][1]:f}) "
163
+ f"image flux = {distances[i][j][2]:f}\n"
164
+ )
165
+ fd.write(str(subimage) + "\n")
166
+
167
+ fd.close()
168
+ return tuple(diff)
169
+
170
+
171
+ def make_point_image(shape, point, value):
172
+ """
173
+ Create an image with a single point set
174
+ """
175
+ output_image = np.zeros(shape, dtype=np.float32)
176
+ output_image[point] = value
177
+ return output_image
178
+
179
+
180
+ def make_grid_image(shape, spacing, value):
181
+ """
182
+ Create an image with points on a grid set
183
+ """
184
+ output_image = np.zeros(shape, dtype=np.float32)
185
+
186
+ shape = output_image.shape
187
+ half_space = int(spacing / 2)
188
+ for y in range(half_space, shape[0], spacing):
189
+ for x in range(half_space, shape[1], spacing):
190
+ output_image[y, x] = value
191
+
192
+ return output_image
193
+
194
+
195
+ @pytest.fixture(scope="module")
196
+ def nrcb5_stars():
197
+ full_file_name = os.path.join(DATA_DIR, "nrcb5_sip_wcs.hdr")
198
+ path = os.path.join(DATA_DIR, full_file_name)
199
+
200
+ wcs, data = wcs_from_file(path, return_data=True)
201
+ dq = np.zeros(data.shape, dtype=np.int32)
202
+ wht = np.zeros(data.shape, dtype=np.float32)
203
+ var = np.zeros(data.shape, dtype=np.float32)
204
+
205
+ np.random.seed(0)
206
+
207
+ patch_size = 21
208
+ patch_area = patch_size**2
209
+ p2 = patch_size // 2
210
+ # add border so that resampled partial pixels can be isolated
211
+ # in the segmentation:
212
+ border = 4
213
+ pwb = patch_size + border
214
+
215
+ fwhm2sigma = 2.0 * math.sqrt(2.0 * math.log(2.0))
216
+
217
+ ny, nx = data.shape
218
+
219
+ stars = []
220
+
221
+ for yc in range(border + p2, ny - pwb, pwb):
222
+ for xc in range(border + p2, nx - pwb, pwb):
223
+ sl = np.s_[yc - p2 : yc + p2 + 1, xc - p2 : xc + p2 + 1]
224
+ flux = 1.0 + 99.0 * np.random.random()
225
+ if np.random.random() > 0.7:
226
+ # uniform image
227
+ psf = np.full((patch_size, patch_size), flux / patch_area)
228
+ else:
229
+ # "star":
230
+ fwhm = 1.5 + 1.5 * np.random.random()
231
+ sigma = fwhm / fwhm2sigma
232
+
233
+ psf = flux * Gaussian2DKernel(sigma, x_size=patch_size, y_size=patch_size).array
234
+ weight = 0.6 + 0.4 * np.random.random((patch_size, patch_size))
235
+ wflux = (psf * weight).sum()
236
+
237
+ mean_noise = (0.05 + 0.35 * np.random.random()) * flux / patch_area
238
+ rdnoise = mean_noise * np.random.random((patch_size, patch_size))
239
+
240
+ data[sl] = psf
241
+ wht[sl] = weight
242
+ dq[sl] = 0
243
+ var_patch = psf + rdnoise
244
+ var[sl] = var_patch
245
+ stars.append((xc, yc, wflux, (var_patch * weight**2).sum(), sl))
246
+
247
+ return data, wht, dq, var, stars, wcs
248
+
249
+
250
+ def test_drizzle_defaults():
251
+ n = 200
252
+ in_shape = (n, n)
253
+
254
+ # input coordinate grid:
255
+ y, x = np.indices(in_shape, dtype=np.float64)
256
+
257
+ # simulate data:
258
+ in_sci = np.ones(in_shape, dtype=np.float32)
259
+ in_wht = np.ones(in_shape, dtype=np.float32)
260
+
261
+ # create a Drizzle object using all default parameters (except for 'kernel')
262
+ driz = resample.Drizzle(
263
+ kernel="square",
264
+ )
265
+
266
+ assert driz.out_img is None
267
+ assert driz.out_wht is None
268
+ assert driz.out_ctx is None
269
+ assert driz.total_exptime == 0.0
270
+
271
+ driz.add_image(
272
+ in_sci,
273
+ exptime=1.0,
274
+ pixmap=np.dstack([x, y]),
275
+ weight_map=in_wht,
276
+ )
277
+
278
+ pixmap = np.dstack([x + 1, y + 2])
279
+ driz.add_image(
280
+ 3 * in_sci,
281
+ exptime=1.0,
282
+ pixmap=pixmap,
283
+ weight_map=in_wht,
284
+ )
285
+
286
+ assert driz.out_img[0, 0] == 1
287
+ assert driz.out_img[1, 0] == 1
288
+ assert driz.out_img[2, 0] == 1
289
+ assert driz.out_img[1, 1] == 1
290
+ assert driz.out_img[1, 2] == 1
291
+ assert (driz.out_img[2, 1] - 2.0) < 1.0e-14
292
+
293
+
294
+ @pytest.mark.parametrize(
295
+ "kernel,test_image_type,max_diff_atol",
296
+ [
297
+ ("square", "point", 1.0e-5),
298
+ ("square", "grid", 1.0e-5),
299
+ ("point", "grid", 1.0e-5),
300
+ ("turbo", "grid", 1.0e-5),
301
+ ("lanczos3", "grid", 1.0e-5),
302
+ ("gaussian", "grid", 2.0e-5),
303
+ ],
304
+ )
305
+ def test_resample_kernel(tmpdir, kernel, test_image_type, max_diff_atol):
306
+ """
307
+ Test do_driz square kernel with point
308
+ """
309
+ output_difference = str(tmpdir.join(f"difference_{kernel}_{test_image_type}.txt"))
310
+
311
+ inwcs = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
312
+ if test_image_type == "point":
313
+ insci = make_point_image(inwcs.array_shape, (500, 200), 100.0)
314
+ else:
315
+ insci = make_grid_image(inwcs.array_shape, 64, 100.0)
316
+ inwht = np.ones_like(insci)
317
+ output_wcs, template_data = wcs_from_file(
318
+ f"reference_{kernel}_{test_image_type}.fits", ext=1, return_data=True
319
+ )
320
+
321
+ pixmap = utils.calc_pixmap(
322
+ inwcs,
323
+ output_wcs,
324
+ )
325
+
326
+ if kernel == "point":
327
+ pscale_ratio = 1.0
328
+ else:
329
+ pscale_ratio = utils.estimate_pixel_scale_ratio(
330
+ inwcs,
331
+ output_wcs,
332
+ refpix_from=inwcs.wcs.crpix,
333
+ refpix_to=output_wcs.wcs.crpix,
334
+ )
335
+
336
+ # ignore previous pscale and compute it the old way (only to make
337
+ # tests work with old truth files and thus to show that new API gives
338
+ # same results when equal definitions of the pixel scale is used):
339
+ pscale_ratio = np.sqrt(
340
+ np.sum(output_wcs.wcs.pc**2, axis=0)[0] / np.sum(inwcs.wcs.cd**2, axis=0)[0]
341
+ )
342
+
343
+ driz = resample.Drizzle(
344
+ kernel=kernel,
345
+ out_shape=output_wcs.array_shape,
346
+ fillval=0.0,
347
+ )
348
+
349
+ if kernel in ["square", "turbo", "point"]:
350
+ driz.add_image(
351
+ insci,
352
+ exptime=1.0,
353
+ pixmap=pixmap,
354
+ weight_map=inwht,
355
+ iscale=pscale_ratio**2,
356
+ pixel_scale_ratio=pscale_ratio,
357
+ )
358
+ else:
359
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
360
+ driz.add_image(
361
+ insci,
362
+ exptime=1.0,
363
+ pixmap=pixmap,
364
+ weight_map=inwht,
365
+ iscale=pscale_ratio**2,
366
+ pixel_scale_ratio=pscale_ratio,
367
+ )
368
+
369
+ _, med_diff, max_diff = centroid_statistics(
370
+ f"{kernel} with {test_image_type}",
371
+ output_difference,
372
+ driz.out_img,
373
+ template_data,
374
+ 30.0,
375
+ 8,
376
+ )
377
+
378
+ assert med_diff < 1.0e-6
379
+ assert max_diff < max_diff_atol
380
+
381
+
382
+ @pytest.mark.parametrize(
383
+ "kernel,max_diff_atol",
384
+ [
385
+ ("square", 1.0e-5),
386
+ ("turbo", 1.0e-5),
387
+ ],
388
+ )
389
+ def test_resample_kernel_image(tmpdir, kernel, max_diff_atol):
390
+ """
391
+ Test do_driz square kernel with point
392
+ """
393
+ inwcs, insci = wcs_from_file("j8bt06nyq_flt.fits", ext=1, return_data=True)
394
+ inwht = np.ones_like(insci)
395
+
396
+ outwcs, ref_sci, ref_ctx, ref_wht = wcs_from_file(
397
+ f"reference_{kernel}_image.fits", ext=1, return_data=["SCI", "CTX", "WHT"]
398
+ )
399
+ ref_ctx = np.array(ref_ctx, dtype=np.int32)
400
+
401
+ pixmap = utils.calc_pixmap(
402
+ inwcs,
403
+ outwcs,
404
+ )
405
+
406
+ pscale_ratio = np.sqrt(np.sum(outwcs.wcs.cd**2, axis=0)[0] / np.sum(inwcs.wcs.cd**2, axis=0)[0])
407
+
408
+ driz = resample.Drizzle(
409
+ kernel=kernel,
410
+ out_shape=ref_sci.shape,
411
+ fillval=0.0,
412
+ )
413
+
414
+ driz.add_image(
415
+ insci,
416
+ exptime=1.0,
417
+ pixmap=pixmap,
418
+ weight_map=inwht,
419
+ iscale=pscale_ratio**2,
420
+ pixel_scale_ratio=pscale_ratio,
421
+ )
422
+ outctx = driz.out_ctx[0]
423
+
424
+ # in order to avoid small differences in the staircase in the outline
425
+ # of the input image in the output grid, select a subset:
426
+ sl = np.s_[125:-125, 5:-5]
427
+
428
+ assert np.allclose(driz.out_img[sl], ref_sci[sl], atol=0, rtol=1.0e-6)
429
+ assert np.allclose(driz.out_wht[sl], ref_wht[sl], atol=0, rtol=1.0e-6)
430
+ assert np.all(outctx[sl] == ref_ctx[sl])
431
+
432
+
433
+ @pytest.mark.parametrize(
434
+ "kernel,fc",
435
+ [
436
+ ("square", True),
437
+ ("point", True),
438
+ ("turbo", True),
439
+ ("lanczos2", False),
440
+ ("lanczos3", False),
441
+ ("gaussian", False),
442
+ ],
443
+ )
444
+ def test_zero_input_weight(kernel, fc):
445
+ """
446
+ Test do_driz square kernel with grid
447
+ """
448
+ # initialize input:
449
+ insci = np.ones((200, 400), dtype=np.float32)
450
+ inwht = np.ones((200, 400), dtype=np.float32)
451
+ inwht[:, 150:155] = 0
452
+
453
+ # initialize output:
454
+ outsci = np.zeros((210, 410), dtype=np.float32)
455
+ outwht = np.zeros((210, 410), dtype=np.float32)
456
+ outctx = np.zeros((210, 410), dtype=np.int32)
457
+
458
+ # define coordinate mapping:
459
+ pixmap = np.moveaxis(np.mgrid[1:201, 1:401][::-1], 0, -1)
460
+
461
+ # resample:
462
+ if fc:
463
+ cdrizzle.tdriz(
464
+ insci,
465
+ inwht,
466
+ pixmap,
467
+ outsci,
468
+ outwht,
469
+ outctx,
470
+ uniqid=1,
471
+ xmin=0,
472
+ xmax=400,
473
+ ymin=0,
474
+ ymax=200,
475
+ pixfrac=1,
476
+ kernel=kernel,
477
+ in_units="cps",
478
+ expscale=1,
479
+ wtscale=1,
480
+ fillstr="INDEF",
481
+ )
482
+ else:
483
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
484
+ cdrizzle.tdriz(
485
+ insci,
486
+ inwht,
487
+ pixmap,
488
+ outsci,
489
+ outwht,
490
+ outctx,
491
+ uniqid=1,
492
+ xmin=0,
493
+ xmax=400,
494
+ ymin=0,
495
+ ymax=200,
496
+ pixfrac=1,
497
+ kernel=kernel,
498
+ in_units="cps",
499
+ expscale=1,
500
+ wtscale=1,
501
+ fillstr="INDEF",
502
+ )
503
+ # pytest.xfail("Not a flux-conserving kernel")
504
+
505
+ # check that no pixel with 0 weight has any counts:
506
+ assert np.sum(np.abs(outsci[(outwht == 0)])) == 0.0
507
+
508
+
509
+ @pytest.mark.parametrize(
510
+ "interpolator,test_image_type",
511
+ [
512
+ ("poly5", "point"),
513
+ ("default", "grid"),
514
+ ("lan3", "grid"),
515
+ ("lan5", "grid"),
516
+ ],
517
+ )
518
+ def test_blot_interpolation(tmpdir, interpolator, test_image_type):
519
+ """
520
+ Test do_driz square kernel with point
521
+ """
522
+ output_difference = str(tmpdir.join(f"difference_blot_{interpolator}_{test_image_type}.txt"))
523
+
524
+ outwcs = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
525
+ if test_image_type == "point":
526
+ outsci = make_point_image(outwcs.array_shape, (500, 200), 40.0)
527
+ ref_fname = "reference_blot_point.fits"
528
+ else:
529
+ outsci = make_grid_image(outwcs.array_shape, 64, 100.0)
530
+ ref_fname = f"reference_blot_{interpolator}.fits"
531
+ inwcs, template_data = wcs_from_file(ref_fname, ext=1, return_data=True)
532
+
533
+ pixmap = utils.calc_pixmap(inwcs, outwcs)
534
+
535
+ # compute pscale the old way (only to make
536
+ # tests work with old truth files and thus to show that new API gives
537
+ # same results when equal definitions of the pixel scale is used):
538
+ pscale_ratio = np.sqrt(np.sum(inwcs.wcs.pc**2, axis=0)[0] / np.sum(outwcs.wcs.cd**2, axis=0)[0])
539
+
540
+ if interpolator == "default":
541
+ kwargs = {}
542
+ else:
543
+ kwargs = {"interp": interpolator}
544
+
545
+ blotted_image = resample.blot_image(
546
+ outsci, pixmap=pixmap, iscale=1.0 / (pscale_ratio**2), **kwargs
547
+ )
548
+
549
+ _, med_diff, max_diff = centroid_statistics(
550
+ "blot with '{interpolator}' and '{test_image_type}'",
551
+ output_difference,
552
+ blotted_image,
553
+ template_data,
554
+ 20.0,
555
+ 16,
556
+ )
557
+ assert med_diff < 1.0e-6
558
+ assert max_diff < 1.0e-5
559
+
560
+
561
+ def test_context_planes():
562
+ """Reproduce error seen in issue #50"""
563
+ shape = (10, 10)
564
+ output_wcs = wcs.WCS()
565
+ output_wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]
566
+ output_wcs.wcs.pc = [[1, 0], [0, 1]]
567
+ output_wcs.pixel_shape = shape
568
+ driz = resample.Drizzle(out_shape=tuple(shape))
569
+
570
+ image = np.ones(shape)
571
+ inwcs = wcs.WCS()
572
+ inwcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]
573
+ inwcs.wcs.cd = [[1, 0], [0, 1]]
574
+ inwcs.pixel_shape = shape
575
+
576
+ pixmap = utils.calc_pixmap(inwcs, output_wcs)
577
+
578
+ # context image must be 2D or 3D:
579
+ with pytest.raises(ValueError) as err_info:
580
+ resample.Drizzle(
581
+ kernel="point",
582
+ exptime=0.0,
583
+ out_shape=shape,
584
+ out_ctx=[0, 0, 0],
585
+ )
586
+ assert str(err_info.value).startswith("'out_ctx' must be either a 2D or 3D array.")
587
+
588
+ driz = resample.Drizzle(
589
+ kernel="square",
590
+ out_shape=output_wcs.array_shape,
591
+ fillval=0.0,
592
+ )
593
+
594
+ for i in range(32):
595
+ assert driz.ctx_id == i
596
+ driz.add_image(image, exptime=1.0, pixmap=pixmap)
597
+ assert driz.out_ctx.shape == (1, 10, 10)
598
+
599
+ driz.add_image(image, exptime=1.0, pixmap=pixmap)
600
+ assert driz.out_ctx.shape == (2, 10, 10)
601
+
602
+
603
+ def test_no_context_image():
604
+ """Reproduce error seen in issue #50"""
605
+ shape = (10, 10)
606
+ output_wcs = wcs.WCS()
607
+ output_wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]
608
+ output_wcs.wcs.pc = [[1, 0], [0, 1]]
609
+ output_wcs.pixel_shape = shape
610
+ driz = resample.Drizzle(out_shape=tuple(shape), begin_ctx_id=-1, disable_ctx=True)
611
+ assert driz.out_ctx is None
612
+ assert driz.ctx_id is None
613
+
614
+ image = np.ones(shape)
615
+ inwcs = wcs.WCS()
616
+ inwcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]
617
+ inwcs.wcs.cd = [[1, 0], [0, 1]]
618
+ inwcs.pixel_shape = shape
619
+
620
+ pixmap = utils.calc_pixmap(inwcs, output_wcs)
621
+
622
+ for i in range(33):
623
+ driz.add_image(image, exptime=1.0, pixmap=pixmap)
624
+ assert driz.out_ctx is None
625
+ assert driz.ctx_id is None
626
+
627
+
628
+ def test_init_ctx_id():
629
+ # starting context ID must be positive
630
+ with pytest.raises(ValueError) as err_info:
631
+ resample.Drizzle(
632
+ kernel="square",
633
+ exptime=0.0,
634
+ begin_ctx_id=-1,
635
+ out_shape=(10, 10),
636
+ )
637
+ assert str(err_info.value).startswith("Invalid context image ID")
638
+
639
+ with pytest.raises(ValueError) as err_info:
640
+ resample.Drizzle(
641
+ kernel="square",
642
+ exptime=0.0,
643
+ out_shape=(10, 10),
644
+ begin_ctx_id=1,
645
+ max_ctx_id=0,
646
+ )
647
+ assert str(err_info.value).startswith("'max_ctx_id' cannot be smaller than 'begin_ctx_id'.")
648
+
649
+
650
+ def test_context_agrees_with_weight():
651
+ n = 200
652
+ out_shape = (n, n)
653
+
654
+ # allocate output arrays:
655
+ out_img = np.zeros(out_shape, dtype=np.float32)
656
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
657
+ out_wht = np.zeros(out_shape, dtype=np.float32)
658
+
659
+ # previous data in weight and context must agree:
660
+ with pytest.raises(ValueError) as err_info:
661
+ out_ctx[0, 0] = 1
662
+ out_ctx[0, 1] = 1
663
+ out_wht[0, 0] = 0.1
664
+ resample.Drizzle(
665
+ kernel="square",
666
+ out_shape=out_shape,
667
+ out_img=out_img,
668
+ out_ctx=out_ctx,
669
+ out_wht=out_wht,
670
+ exptime=1.0,
671
+ )
672
+ assert str(err_info.value).startswith(
673
+ "Inconsistent values of supplied 'out_wht' and 'out_ctx' "
674
+ )
675
+
676
+
677
+ @pytest.mark.parametrize(
678
+ "kernel,fc,pixel_scale_ratio",
679
+ [
680
+ ("square", True, 1.0),
681
+ ("point", True, 1.0),
682
+ ("turbo", True, 1.0),
683
+ ("turbo", True, None),
684
+ ("lanczos2", False, 1.0),
685
+ ("lanczos2", False, None),
686
+ ("lanczos3", False, 1.0),
687
+ ("lanczos3", False, None),
688
+ ("gaussian", False, 1.0),
689
+ ("gaussian", False, None),
690
+ ],
691
+ )
692
+ def test_flux_conservation_nondistorted(kernel, fc, pixel_scale_ratio):
693
+ n = 200
694
+ in_shape = (n, n)
695
+
696
+ # input coordinate grid:
697
+ y, x = np.indices(in_shape, dtype=np.float64)
698
+
699
+ # simulate a gaussian "star":
700
+ fwhm = 2.9
701
+ x0 = 50.0
702
+ y0 = 68.0
703
+ sig = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0 * fwhm)))
704
+ sig2 = sig * sig
705
+ star = np.exp(
706
+ -0.5 / sig2 * ((x.astype(np.float32) - x0) ** 2 + (y.astype(np.float32) - y0) ** 2)
707
+ )
708
+ in_sci = (star / np.sum(star)).astype(np.float32) # normalize to 1
709
+ in_wht = np.ones(in_shape, dtype=np.float32)
710
+
711
+ # linear shift:
712
+ xp = x + 0.5
713
+ yp = y + 0.2
714
+
715
+ pixmap = np.dstack([xp, yp])
716
+
717
+ out_shape = (int(yp.max()) + 1, int(xp.max()) + 1)
718
+ # make sure distorion is not moving flux out of the image towards negative
719
+ # coordinates (just because of the simple way of how we account for output
720
+ # image size)
721
+ assert np.min(xp) > -0.5 and np.min(yp) > -0.5
722
+
723
+ out_img = np.zeros(out_shape, dtype=np.float32)
724
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
725
+ out_wht = np.zeros(out_shape, dtype=np.float32)
726
+
727
+ if fc:
728
+ cdrizzle.tdriz(
729
+ in_sci,
730
+ in_wht,
731
+ pixmap,
732
+ out_img,
733
+ out_wht,
734
+ out_ctx,
735
+ pixfrac=1.0,
736
+ pscale_ratio=pixel_scale_ratio,
737
+ kernel=kernel,
738
+ in_units="cps",
739
+ expscale=1.0,
740
+ wtscale=1.0,
741
+ )
742
+ else:
743
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
744
+ cdrizzle.tdriz(
745
+ in_sci,
746
+ in_wht,
747
+ pixmap,
748
+ out_img,
749
+ out_wht,
750
+ out_ctx,
751
+ pixfrac=1.0,
752
+ pscale_ratio=pixel_scale_ratio,
753
+ kernel=kernel,
754
+ in_units="cps",
755
+ expscale=1.0,
756
+ wtscale=1.0,
757
+ )
758
+
759
+ pytest.xfail("Not a flux-conserving kernel")
760
+
761
+ assert np.allclose(
762
+ np.sum(out_img * out_wht),
763
+ np.sum(in_sci),
764
+ atol=0.0,
765
+ rtol=0.0001,
766
+ )
767
+
768
+
769
+ @pytest.mark.parametrize(
770
+ "kernel,fc",
771
+ [
772
+ ("square", True),
773
+ ("point", True),
774
+ ("turbo", True),
775
+ ("lanczos2", False),
776
+ ("lanczos3", False),
777
+ ("gaussian", False),
778
+ ],
779
+ )
780
+ def test_flux_conservation_distorted(kernel, fc):
781
+ n = 200
782
+ in_shape = (n, n)
783
+
784
+ # input coordinate grid:
785
+ y, x = np.indices(in_shape, dtype=np.float64)
786
+
787
+ # simulate a gaussian "star":
788
+ fwhm = 2.9
789
+ x0 = 50.0
790
+ y0 = 68.0
791
+ sig = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0 * fwhm)))
792
+ sig2 = sig * sig
793
+ star = np.exp(
794
+ -0.5 / sig2 * ((x.astype(np.float32) - x0) ** 2 + (y.astype(np.float32) - y0) ** 2)
795
+ )
796
+ in_sci = (star / np.sum(star)).astype(np.float32) # normalize to 1
797
+ in_wht = np.ones(in_shape, dtype=np.float32)
798
+
799
+ # linear shift:
800
+ xp = x + 0.5
801
+ yp = y + 0.2
802
+ # add distortion:
803
+ xp += 1e-4 * x**2 + 1e-5 * x * y
804
+ yp += 1e-3 * y**2 - 2e-5 * x * y
805
+
806
+ pixmap = np.dstack([xp, yp])
807
+
808
+ out_shape = (int(yp.max()) + 1, int(xp.max()) + 1)
809
+ # make sure distorion is not moving (pixels with) flux out of the image
810
+ # towards negative coordinates (just because of the simple way of how we
811
+ # account for output image size):
812
+ assert np.min(xp) > -0.5 and np.min(yp) > -0.5
813
+
814
+ out_img = np.zeros(out_shape, dtype=np.float32)
815
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
816
+ out_wht = np.zeros(out_shape, dtype=np.float32)
817
+
818
+ if fc:
819
+ cdrizzle.tdriz(
820
+ in_sci,
821
+ in_wht,
822
+ pixmap,
823
+ out_img,
824
+ out_wht,
825
+ out_ctx,
826
+ pixfrac=1.0,
827
+ pscale_ratio=1.0,
828
+ kernel=kernel,
829
+ in_units="cps",
830
+ expscale=1.0,
831
+ wtscale=1.0,
832
+ )
833
+ else:
834
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
835
+ cdrizzle.tdriz(
836
+ in_sci,
837
+ in_wht,
838
+ pixmap,
839
+ out_img,
840
+ out_wht,
841
+ out_ctx,
842
+ pixfrac=1.0,
843
+ pscale_ratio=1.0,
844
+ kernel=kernel,
845
+ in_units="cps",
846
+ expscale=1.0,
847
+ wtscale=1.0,
848
+ )
849
+ pytest.xfail("Not a flux-conserving kernel")
850
+
851
+ assert np.allclose(
852
+ np.sum(out_img * out_wht),
853
+ np.sum(in_sci),
854
+ atol=0.0,
855
+ rtol=0.0001,
856
+ )
857
+
858
+
859
+ @pytest.mark.parametrize("kernel", ["square", "turbo", "point"])
860
+ @pytest.mark.parametrize("pscale_ratio", [0.55, 1.0, 1.2])
861
+ def test_flux_conservation_distorted_distributed_sources(nrcb5_stars, kernel, pscale_ratio):
862
+ """test aperture photometry"""
863
+ insci, inwht, _, invar, stars, wcs = nrcb5_stars
864
+
865
+ suffix = f"{pscale_ratio}".replace(".", "p")
866
+ output_wcs = wcs_from_file(f"nrcb5_output_wcs_psr_{suffix}.hdr")
867
+
868
+ pixmap = utils.calc_pixmap(
869
+ wcs,
870
+ output_wcs,
871
+ wcs.array_shape,
872
+ )
873
+
874
+ # resample variance using squared coefficients AND a "point" kernel
875
+ # ("point" kernel is needed for the *aperture* photometry/variance)
876
+ driz_var = resample.Drizzle(
877
+ kernel="point",
878
+ out_shape=output_wcs.array_shape,
879
+ fillval=0.0,
880
+ )
881
+ driz_var.add_image(
882
+ data=insci,
883
+ data2=invar,
884
+ exptime=1.0,
885
+ pixmap=pixmap,
886
+ weight_map=inwht,
887
+ iscale=1.0,
888
+ pixel_scale_ratio=1.0,
889
+ )
890
+
891
+ # for efficiency, instead of doing this patch-by-patch,
892
+ # multiply resampled data by resampled image weight
893
+ if kernel == "point":
894
+ out_data = driz_var.out_img * driz_var.out_wht
895
+ else:
896
+ # resample "SCI" array using user-specified kernel:
897
+ driz = resample.Drizzle(
898
+ kernel=kernel,
899
+ out_shape=output_wcs.array_shape,
900
+ fillval=0.0,
901
+ )
902
+ driz.add_image(
903
+ data=insci,
904
+ exptime=1.0,
905
+ pixmap=pixmap,
906
+ weight_map=inwht,
907
+ iscale=1.0,
908
+ pixel_scale_ratio=1.0,
909
+ )
910
+ out_data = driz_var.out_img * driz_var.out_wht
911
+ out_var = driz_var.out_img2[0] * (driz_var.out_wht**2)
912
+
913
+ dim3 = (slice(None, None, None),)
914
+
915
+ for _, _, wfin, wvfin, sl in stars:
916
+ xyout = pixmap[sl + dim3]
917
+ xmin = math.floor(xyout[:, :, 0].min() - 0.5)
918
+ xmax = math.ceil(xyout[:, :, 0].max() + 1.5)
919
+ ymin = math.floor(xyout[:, :, 1].min() - 0.5)
920
+ ymax = math.ceil(xyout[:, :, 1].max() + 1.5)
921
+
922
+ wfout = np.nansum(out_data[ymin:ymax, xmin:xmax])
923
+ wvfout = np.nansum(out_var[ymin:ymax, xmin:xmax])
924
+
925
+ # test resampled "weighted" flux in an aperture matches flux from input
926
+ assert np.allclose(wfin, wfout, rtol=1.0e-6, atol=0.0)
927
+
928
+ # test resampled "weighted" variance in an aperture matches
929
+ # "weighted" variance from input
930
+ assert np.allclose(wvfin, wvfout, rtol=1.0e-6, atol=0.0)
931
+
932
+
933
+ def test_drizzle_exptime():
934
+ n = 200
935
+ in_shape = (n, n)
936
+
937
+ # input coordinate grid:
938
+ y, x = np.indices(in_shape, dtype=np.float64)
939
+
940
+ # simulate data:
941
+ in_sci = np.ones(in_shape, dtype=np.float32)
942
+ in_wht = np.ones(in_shape, dtype=np.float32)
943
+
944
+ pixmap = np.dstack([x, y])
945
+
946
+ # allocate output arrays:
947
+ out_shape = (int(y.max()) + 1, int(x.max()) + 1)
948
+ out_img = np.zeros(out_shape, dtype=np.float32)
949
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
950
+ out_wht = np.zeros(out_shape, dtype=np.float32)
951
+
952
+ # starting exposure time must be non-negative:
953
+ with pytest.raises(ValueError) as err_info:
954
+ driz = resample.Drizzle(
955
+ kernel="square",
956
+ out_shape=out_shape,
957
+ fillval="indef",
958
+ exptime=-1.0,
959
+ )
960
+ assert str(err_info.value) == "Exposure time must be non-negative."
961
+
962
+ driz = resample.Drizzle(
963
+ kernel="turbo",
964
+ out_shape=out_shape,
965
+ fillval="",
966
+ out_img=out_img,
967
+ out_ctx=out_ctx,
968
+ out_wht=out_wht,
969
+ exptime=1.0,
970
+ )
971
+ assert driz.kernel == "turbo"
972
+
973
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.03456, pixmap=pixmap)
974
+ assert np.allclose(driz.total_exptime, 2.03456, rtol=0, atol=1.0e-14)
975
+
976
+ driz.add_image(in_sci, weight_map=in_wht, exptime=3.1415926, pixmap=pixmap)
977
+ assert np.allclose(driz.total_exptime, 5.1761526, rtol=0, atol=1.0e-14)
978
+
979
+ with pytest.raises(ValueError) as err_info:
980
+ driz.add_image(in_sci, weight_map=in_wht, exptime=-1, pixmap=pixmap)
981
+ assert str(err_info.value) == "'exptime' *must* be a strictly positive number."
982
+
983
+ # exptime cannot be 0 when output data has data:
984
+ with pytest.raises(ValueError) as err_info:
985
+ out_ctx[0, 0] = 1
986
+ driz = resample.Drizzle(
987
+ kernel="square",
988
+ out_shape=out_shape,
989
+ fillval="indef",
990
+ out_img=out_img,
991
+ out_ctx=out_ctx,
992
+ out_wht=out_wht,
993
+ exptime=0.0,
994
+ )
995
+ assert str(err_info.value).startswith(
996
+ "Inconsistent exposure time and context and/or weight images:"
997
+ )
998
+
999
+ # exptime must be 0 when output arrays are not provided:
1000
+ with pytest.raises(ValueError) as err_info:
1001
+ driz = resample.Drizzle(
1002
+ kernel="square",
1003
+ out_shape=out_shape,
1004
+ exptime=1.0,
1005
+ )
1006
+ assert str(err_info.value).startswith("Exposure time must be 0.0 for the first resampling")
1007
+
1008
+
1009
+ def test_drizzle_unsupported_kernel():
1010
+ with pytest.raises(ValueError) as err_info:
1011
+ resample.Drizzle(
1012
+ kernel="magic_image_improver",
1013
+ out_shape=(10, 10),
1014
+ exptime=0.0,
1015
+ )
1016
+ assert str(err_info.value) == "Kernel 'magic_image_improver' is not supported."
1017
+
1018
+
1019
+ def test_pixmap_shape_matches_image():
1020
+ n = 200
1021
+ in_shape = (n, n)
1022
+
1023
+ # input coordinate grid:
1024
+ y, x = np.indices((n + 1, n), dtype=np.float64)
1025
+
1026
+ # simulate data:
1027
+ in_sci = np.ones(in_shape, dtype=np.float32)
1028
+ in_wht = np.ones(in_shape, dtype=np.float32)
1029
+
1030
+ pixmap = np.dstack([x, y])
1031
+
1032
+ driz = resample.Drizzle(
1033
+ kernel="square",
1034
+ fillval=0.0,
1035
+ exptime=0.0,
1036
+ )
1037
+
1038
+ # last two sizes of the pixelmap must match those of input images:
1039
+ with pytest.raises(ValueError) as err_info:
1040
+ driz.add_image(
1041
+ in_sci,
1042
+ exptime=1.0,
1043
+ pixmap=pixmap,
1044
+ weight_map=in_wht,
1045
+ iscale=1.0,
1046
+ pixel_scale_ratio=1.0,
1047
+ )
1048
+ assert str(err_info.value) == "'pixmap' shape is not consistent with 'data' shape."
1049
+
1050
+
1051
+ def test_drizzle_fillval():
1052
+ n = 200
1053
+ in_shape = (n, n)
1054
+
1055
+ # input coordinate grid:
1056
+ y, x = np.indices(in_shape, dtype=np.float64)
1057
+
1058
+ # simulate a gaussian "star":
1059
+ fwhm = 2.9
1060
+ x0 = 50.0
1061
+ y0 = 68.0
1062
+ sig = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0 * fwhm)))
1063
+ sig2 = sig * sig
1064
+ star = np.exp(
1065
+ -0.5 / sig2 * ((x.astype(np.float32) - x0) ** 2 + (y.astype(np.float32) - y0) ** 2)
1066
+ )
1067
+ in_sci = (star / np.sum(star)).astype(np.float32) # normalize to 1
1068
+ in_wht = np.zeros(in_shape, dtype=np.float32)
1069
+ mask = np.where((x.astype(np.float32) - x0) ** 2 + (y.astype(np.float32) - y0) ** 2 <= 10)
1070
+ in_wht[mask] = 1.0
1071
+
1072
+ # linear shift:
1073
+ xp = x + 50
1074
+ yp = y + 50
1075
+
1076
+ pixmap = np.dstack([xp, yp])
1077
+
1078
+ out_shape = (int(yp.max()) + 1, int(xp.max()) + 1)
1079
+ # make sure distorion is not moving flux out of the image towards negative
1080
+ # coordinates (just because of the simple way of how we account for output
1081
+ # image size)
1082
+ assert np.min(xp) > -0.5 and np.min(yp) > -0.5
1083
+
1084
+ out_img = np.zeros(out_shape, dtype=np.float32) - 1.11
1085
+ out_ctx = np.zeros((1,) + out_shape, dtype=np.int32)
1086
+ out_wht = np.zeros(out_shape, dtype=np.float32)
1087
+
1088
+ driz = resample.Drizzle(
1089
+ kernel="square",
1090
+ out_shape=out_shape,
1091
+ fillval="indef",
1092
+ exptime=0.0,
1093
+ )
1094
+
1095
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap)
1096
+ assert np.isnan(driz.out_img[0, 0])
1097
+ assert driz.out_img[int(y0) + 50, int(x0) + 50] > 0.0
1098
+
1099
+ driz = resample.Drizzle(
1100
+ kernel="square",
1101
+ out_shape=out_shape,
1102
+ fillval="-1.11",
1103
+ out_img=out_img.copy(),
1104
+ out_ctx=out_ctx.copy(),
1105
+ out_wht=out_wht.copy(),
1106
+ exptime=0.0,
1107
+ )
1108
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap)
1109
+ assert np.allclose(driz.out_img[0, 0], -1.11, rtol=0.0, atol=1.0e-7)
1110
+ assert driz.out_img[int(y0) + 50, int(x0) + 50] > 0.0
1111
+ assert set(driz.out_ctx.ravel().tolist()) == {0, 1}
1112
+
1113
+ # test same with numeric fillval:
1114
+ driz = resample.Drizzle(
1115
+ kernel="square",
1116
+ out_shape=out_shape,
1117
+ fillval=-1.11,
1118
+ out_img=out_img.copy(),
1119
+ out_ctx=out_ctx.copy(),
1120
+ out_wht=out_wht.copy(),
1121
+ exptime=0.0,
1122
+ )
1123
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap)
1124
+ assert np.allclose(driz.out_img[0, 0], -1.11, rtol=0.0, atol=1.0e-7)
1125
+ assert np.allclose(float(driz.fillval), -1.11, rtol=0.0, atol=np.finfo(float).eps)
1126
+
1127
+ # make sure code raises exception for unsupported fillval:
1128
+ with pytest.raises(ValueError) as err_info:
1129
+ resample.Drizzle(
1130
+ kernel="square",
1131
+ out_shape=out_shape,
1132
+ fillval="fillval",
1133
+ exptime=0.0,
1134
+ )
1135
+ assert str(err_info.value) == "could not convert string to float: 'fillval'"
1136
+
1137
+
1138
+ def test_resample_get_shape_from_pixmap():
1139
+ n = 200
1140
+ in_shape = (n, n)
1141
+
1142
+ # input coordinate grid:
1143
+ y, x = np.indices(in_shape, dtype=np.float64)
1144
+
1145
+ # simulate constant data:
1146
+ in_sci = np.ones(in_shape, dtype=np.float32)
1147
+ in_wht = np.ones(in_shape, dtype=np.float32)
1148
+
1149
+ pixmap = np.dstack([x, y])
1150
+
1151
+ driz = resample.Drizzle(
1152
+ kernel="point",
1153
+ exptime=0.0,
1154
+ )
1155
+
1156
+ driz.add_image(in_sci, weight_map=in_wht, exptime=0.1, pixmap=pixmap)
1157
+ assert driz.out_img.shape == in_shape
1158
+
1159
+
1160
+ def test_resample_counts_units():
1161
+ n = 200
1162
+ in_shape = (n, n)
1163
+
1164
+ # input coordinate grid:
1165
+ y, x = np.indices(in_shape, dtype=np.float64)
1166
+ pixmap = np.dstack([x, y])
1167
+
1168
+ # simulate constant data:
1169
+ in_sci = np.ones(in_shape, dtype=np.float32)
1170
+ in_wht = np.ones(in_shape, dtype=np.float32)
1171
+
1172
+ driz = resample.Drizzle()
1173
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap, in_units="cps")
1174
+ cps_max_val = driz.out_img.max()
1175
+
1176
+ driz = resample.Drizzle()
1177
+ driz.add_image(in_sci, weight_map=in_wht, exptime=2.0, pixmap=pixmap, in_units="counts")
1178
+ counts_max_val = driz.out_img.max()
1179
+
1180
+ assert abs(counts_max_val - cps_max_val / 2.0) < 1.0e-14
1181
+
1182
+
1183
+ def test_resample_inconsistent_output():
1184
+ n = 200
1185
+ out_shape = (n, n)
1186
+
1187
+ # different shapes:
1188
+ out_img = np.zeros((n, n), dtype=np.float32)
1189
+ out_ctx = np.zeros((1, n, n + 1), dtype=np.int32)
1190
+ out_wht = np.zeros((n + 1, n + 1), dtype=np.float32)
1191
+
1192
+ # shape from out_img:
1193
+ driz = resample.Drizzle(
1194
+ kernel="point",
1195
+ exptime=0.0,
1196
+ out_img=out_img,
1197
+ )
1198
+ assert driz.out_img.shape == out_shape
1199
+
1200
+ # inconsistent shapes:
1201
+ out_shape = (n + 1, n)
1202
+ with pytest.raises(ValueError) as err_info:
1203
+ resample.Drizzle(
1204
+ kernel="point",
1205
+ exptime=0.0,
1206
+ out_shape=out_shape,
1207
+ out_img=out_img,
1208
+ out_ctx=out_ctx,
1209
+ out_wht=out_wht,
1210
+ )
1211
+ assert str(err_info.value).startswith("Inconsistent data shapes specified")
1212
+
1213
+
1214
+ def test_resample_disable_ctx():
1215
+ n = 20
1216
+ in_shape = (n, n)
1217
+
1218
+ pixmap = np.dstack(np.indices(in_shape, dtype=np.float64)[::-1])
1219
+
1220
+ # simulate constant data:
1221
+ in_sci = np.ones(in_shape, dtype=np.float32)
1222
+
1223
+ driz = resample.Drizzle(
1224
+ disable_ctx=True,
1225
+ )
1226
+
1227
+ driz.add_image(in_sci, exptime=1.0, pixmap=pixmap)
1228
+
1229
+
1230
+ @pytest.mark.parametrize("fillval", ["NaN", "INDEF", "", None])
1231
+ def test_nan_fillval(fillval):
1232
+ driz = resample.Drizzle(kernel="square", fillval=fillval, out_shape=(20, 20))
1233
+
1234
+ assert np.all(np.isnan(driz.out_img))
1235
+
1236
+
1237
+ def test_resample_edge_sgarea_bug():
1238
+ """
1239
+ Test from https://github.com/spacetelescope/drizzle/issues/187
1240
+
1241
+ """
1242
+ pixmap = np.array(
1243
+ [
1244
+ [
1245
+ [0.31887051, 1.0],
1246
+ [1.01898591, 1.0],
1247
+ [1.71909665, 1.0],
1248
+ ],
1249
+ [
1250
+ [0.31591881, 0.0],
1251
+ [1.0160342312345672, 0.0],
1252
+ [1.716145, 0.0],
1253
+ ],
1254
+ ],
1255
+ dtype="f8",
1256
+ )
1257
+
1258
+ in_shape = pixmap.shape[:2]
1259
+ img = np.full(in_shape, 42, dtype=np.float32)
1260
+ out_shape = (4, 4)
1261
+
1262
+ driz = resample.Drizzle(
1263
+ kernel="square",
1264
+ fillval="nan",
1265
+ out_shape=out_shape,
1266
+ disable_ctx=True,
1267
+ )
1268
+
1269
+ driz.add_image(
1270
+ img,
1271
+ exptime=11.776,
1272
+ in_units="cps",
1273
+ pixfrac=1.0,
1274
+ pixmap=pixmap,
1275
+ iscale=1.0,
1276
+ pixel_scale_ratio=1.0,
1277
+ wht_scale=1.0,
1278
+ )
1279
+ # expected pixels should be close to 42
1280
+ np.testing.assert_allclose(driz.out_img[:2, :3], img[0, 0], rtol=1e-6)
1281
+
1282
+ # other values should be nan
1283
+ np.testing.assert_equal(driz.out_img[:, 3:], np.nan)
1284
+ np.testing.assert_equal(driz.out_img[2:], np.nan)
1285
+
1286
+
1287
+ def test_resample_edge_collinear():
1288
+ """
1289
+ Test that resample does not crash when the input image is smaller than the
1290
+ output image, and the edges of the two images are nearly collinear.
1291
+
1292
+ Test based on the example from
1293
+ https://github.com/spacetelescope/drizzle/issues/189#issue-3196294879
1294
+
1295
+ """
1296
+ pixmap = np.array(
1297
+ [
1298
+ [
1299
+ [0.31, 1.0],
1300
+ [1.01, 1.0],
1301
+ [2.01, 1.0],
1302
+ ],
1303
+ [
1304
+ [0.31, 0.0],
1305
+ [1.01, 0.0],
1306
+ [1.71, 0.0],
1307
+ ],
1308
+ ],
1309
+ dtype="f8",
1310
+ )
1311
+
1312
+ in_shape = pixmap.shape[:2]
1313
+ img = np.full(in_shape, np.pi, dtype=np.float32)
1314
+ in_flux = np.sum(img)
1315
+ out_shape = (4, 4)
1316
+
1317
+ driz = resample.Drizzle(
1318
+ kernel="square",
1319
+ fillval="nan",
1320
+ out_shape=out_shape,
1321
+ disable_ctx=True,
1322
+ )
1323
+
1324
+ driz.add_image(
1325
+ img,
1326
+ exptime=11.776,
1327
+ in_units="cps",
1328
+ pixfrac=1.0,
1329
+ pixmap=pixmap,
1330
+ iscale=1.0,
1331
+ pixel_scale_ratio=1.0,
1332
+ wht_scale=1.0,
1333
+ )
1334
+
1335
+ out_flux = np.nansum(driz.out_img * driz.out_wht)
1336
+
1337
+ # Given this pixmap, the entire input image should fit within the output
1338
+ # image. There should be at least 7 pixels with finite values in the output
1339
+ # image. We can get more than 7 pixels with finite values due to rounding
1340
+ # errors when computing polygon intersections (those "extra" pixels should)
1341
+ # have very small weights.
1342
+ assert np.sum(driz.out_wht > 1e-30) == 7
1343
+ assert np.sum(np.isfinite(driz.out_img)) >= 7
1344
+ # output image intensity must be equal to the input image intensity:
1345
+ assert np.allclose(driz.out_img[np.isfinite(driz.out_img)], img[0, 0], rtol=0, atol=1e-6)
1346
+ # flux in the output image should be equal to the flux in the input image:
1347
+ assert np.allclose(out_flux, in_flux, rtol=1e-6, atol=0.0)
1348
+ # area of the signal in the input image:
1349
+ assert np.allclose(np.sum(driz.out_wht), 6.0, rtol=0, atol=1.0e-6)
1350
+
1351
+
1352
+ @pytest.mark.parametrize(
1353
+ "kernel,fc",
1354
+ [
1355
+ ("square", True),
1356
+ ("point", True),
1357
+ ("turbo", True),
1358
+ ("lanczos2", False),
1359
+ ("lanczos3", False),
1360
+ ("gaussian", False),
1361
+ ],
1362
+ )
1363
+ def test_drizzle_weights_squared(kernel, fc):
1364
+ n = 17
1365
+ in_shape = (n, n)
1366
+
1367
+ # input coordinate grid:
1368
+ y, x = np.indices(in_shape, dtype=np.float64)
1369
+
1370
+ in_sci1 = np.zeros(in_shape, dtype=np.float32)
1371
+ in_wht1 = np.zeros(in_shape, dtype=np.float32)
1372
+ in_sci1_sq = np.zeros(in_shape, dtype=np.float32)
1373
+
1374
+ in_sci2 = np.zeros(in_shape, dtype=np.float32)
1375
+ in_wht2 = np.zeros(in_shape, dtype=np.float32)
1376
+ in_sci2_sq = np.zeros(in_shape, dtype=np.float32)
1377
+
1378
+ xc = yc = n // 2
1379
+
1380
+ in_sci1[yc, xc] = 1.0
1381
+ in_wht1[yc, xc] = 0.99
1382
+ in_sci1_sq[yc, xc] = 0.5
1383
+
1384
+ in_sci2[yc, xc] = 7.0
1385
+ in_wht2[yc, xc] = 0.01
1386
+ in_sci2_sq[yc, xc] = 50.0
1387
+
1388
+ pixmap = np.dstack([x, y])
1389
+
1390
+ out_shape = (int(y.max()) + 1, int(x.max()) + 1)
1391
+
1392
+ if fc:
1393
+ # create a Drizzle object using all default parameters
1394
+ # (except for 'kernel', 'out_shape')
1395
+ driz = resample.Drizzle(
1396
+ kernel=kernel,
1397
+ out_shape=out_shape,
1398
+ fillval2=-99.0,
1399
+ )
1400
+
1401
+ assert driz.out_img is not None
1402
+ assert driz.out_img2 is None
1403
+ assert driz.total_exptime == 0.0
1404
+
1405
+ driz.add_image(
1406
+ data=in_sci1,
1407
+ exptime=1.0,
1408
+ pixmap=pixmap,
1409
+ weight_map=in_wht1,
1410
+ data2=[in_sci1_sq],
1411
+ )
1412
+
1413
+ driz.add_image(
1414
+ data=in_sci2,
1415
+ exptime=1.0,
1416
+ pixmap=pixmap,
1417
+ weight_map=in_wht2,
1418
+ data2=in_sci2_sq,
1419
+ )
1420
+
1421
+ assert isinstance(driz.out_img2, list)
1422
+ assert len(driz.out_img2) == 1
1423
+
1424
+ else:
1425
+ # create a Drizzle object using mostly default parameters
1426
+ driz = resample.Drizzle(
1427
+ kernel=kernel,
1428
+ out_img2=[None],
1429
+ fillval2=-99.0,
1430
+ )
1431
+
1432
+ assert driz.out_img is None
1433
+ assert driz.total_exptime == 0.0
1434
+
1435
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
1436
+ driz.add_image(
1437
+ data=in_sci1,
1438
+ exptime=1.0,
1439
+ pixmap=pixmap,
1440
+ weight_map=in_wht1,
1441
+ data2=[in_sci1_sq],
1442
+ )
1443
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
1444
+ driz.add_image(
1445
+ data=in_sci2,
1446
+ exptime=1.0,
1447
+ pixmap=pixmap,
1448
+ weight_map=in_wht2,
1449
+ data2=[in_sci2_sq],
1450
+ )
1451
+
1452
+ assert np.allclose(np.max(driz.out_img2), 0.495050013, rtol=1.0e-6, atol=0.0)
1453
+
1454
+ # check fill value
1455
+ assert np.allclose(np.min(driz.out_img2), -99.0, rtol=1.0e-6, atol=0.0)
1456
+ assert abs(float(driz.fillval2) + 99.0) < 1e-7
1457
+
1458
+
1459
+ @pytest.mark.filterwarnings("ignore:Kernel '")
1460
+ @pytest.mark.parametrize(
1461
+ "kernel_fc, pscale, weights",
1462
+ (
1463
+ x
1464
+ for x in product(
1465
+ [
1466
+ ("square", True),
1467
+ ("turbo", True),
1468
+ ("point", True),
1469
+ ("gaussian", False),
1470
+ # lanczos kernels do not support pscale != 1 or pixfrac != 1
1471
+ # ('lanczos2', False),
1472
+ # ('lanczos3', False),
1473
+ ],
1474
+ [0.25, 0.5, 1, 1.2, 1.5],
1475
+ [(0.99, 0.01), (0.8, 0.2), (0.9, 1.5), (467, 733)],
1476
+ )
1477
+ ),
1478
+ )
1479
+ def test_drizzle_weights_squared_pscale(kernel_fc, pscale, weights):
1480
+ n = 25
1481
+ shape = (n, n)
1482
+
1483
+ # unpack parameters:
1484
+ kernel, fc = kernel_fc
1485
+
1486
+ # pixel values in input data:
1487
+ dataval = [1.0, 7.0]
1488
+
1489
+ # pixel values in input variance:
1490
+ varval = [0.5, 50]
1491
+
1492
+ # input coordinate grid:
1493
+ y, x = np.indices(shape, dtype=np.float64)
1494
+ pixmap = np.dstack([x, y]) / pscale
1495
+
1496
+ data = [np.zeros(shape, dtype=np.float32) for _ in range(2)]
1497
+ weight = [np.zeros(shape, dtype=np.float32) for _ in range(2)]
1498
+ var = [np.zeros(shape, dtype=np.float32) for _ in range(2)]
1499
+
1500
+ xc = yc = n // 2
1501
+ sl = np.s_[yc - 4 : yc + 5, xc - 4 : xc + 5]
1502
+ for k in range(2):
1503
+ data[k][sl] = dataval[k]
1504
+ weight[k][sl] = weights[k]
1505
+ var[k][sl] = varval[k]
1506
+
1507
+ out_shape = (int(pixmap[..., 1].max()) + 1, int(pixmap[..., 0].max()) + 1)
1508
+
1509
+ # create a Drizzle object
1510
+ driz = resample.Drizzle(
1511
+ kernel=kernel,
1512
+ out_shape=out_shape,
1513
+ fillval=0.0,
1514
+ fillval2=0.0,
1515
+ )
1516
+
1517
+ # resample & add input images
1518
+ for k in range(2):
1519
+ driz.add_image(
1520
+ data=data[k],
1521
+ exptime=1.0,
1522
+ pixmap=pixmap,
1523
+ weight_map=weight[k],
1524
+ data2=var[k],
1525
+ )
1526
+
1527
+ mask = driz.out_ctx[0] > 0
1528
+ n_nonzero = np.sum(data[0] > 0.0)
1529
+
1530
+ rtol = 1.0e-6 if fc else 0.15
1531
+
1532
+ ideal_output = np.dot(dataval, weights) * n_nonzero
1533
+ ideal_output2 = np.dot(varval, np.square(weights)) / np.sum(weights) ** 2
1534
+
1535
+ tflux = np.sum(driz.out_img[mask] * driz.out_wht[mask])
1536
+ tflux2 = np.max(driz.out_img2[0])
1537
+
1538
+ # check output flux:
1539
+ assert np.allclose(tflux, ideal_output, rtol=rtol, atol=0.0)
1540
+
1541
+ # check output variance:
1542
+ # less restrictive (to account for pixel overlap variations):
1543
+ assert np.max(tflux2) <= ideal_output2 * (1 + rtol) and np.max(
1544
+ tflux2
1545
+ ) >= 0.25 * ideal_output2 * (1 - rtol)
1546
+
1547
+ # more restrictive check assumes pixels have good overlaps:
1548
+ assert np.allclose(tflux2, ideal_output2, rtol=rtol, atol=0.0)
1549
+
1550
+
1551
+ def test_drizzle_weights_squared_bad_inputs():
1552
+ n = 21
1553
+ in_shape = (n, n)
1554
+ kernel = "square"
1555
+
1556
+ # input coordinate grid:
1557
+ y, x = np.indices(in_shape, dtype=np.float64)
1558
+
1559
+ in_sci1 = np.zeros(in_shape, dtype=np.float32)
1560
+ in_wht1 = np.zeros(in_shape, dtype=np.float32)
1561
+ in_sci1_sq = np.zeros(in_shape, dtype=np.float32)
1562
+
1563
+ in_sci2 = np.zeros(in_shape, dtype=np.float32)
1564
+ in_wht2 = np.zeros(in_shape, dtype=np.float32)
1565
+ in_sci2_sq = np.zeros(in_shape, dtype=np.float32)
1566
+
1567
+ pixmap = np.dstack([x, y])
1568
+
1569
+ out_shape = (int(y.max()) + 1, int(x.max()) + 1)
1570
+
1571
+ out_img = np.zeros(out_shape, dtype=np.float32)
1572
+ out_img2 = np.zeros(out_shape, dtype=np.float32)
1573
+ out_img2b = np.zeros(out_shape, dtype=np.float32)
1574
+
1575
+ # 1 - test same number of data2 is used each time:
1576
+ driz = resample.Drizzle(
1577
+ kernel=kernel,
1578
+ )
1579
+
1580
+ assert driz.out_img is None
1581
+ assert driz.out_img2 is None
1582
+ assert driz.total_exptime == 0.0
1583
+
1584
+ driz.add_image(
1585
+ data=in_sci1,
1586
+ exptime=1.0,
1587
+ pixmap=pixmap,
1588
+ weight_map=in_wht1,
1589
+ data2=None,
1590
+ )
1591
+ assert driz.out_img2 is None
1592
+
1593
+ with pytest.raises(ValueError) as err_info:
1594
+ driz.add_image(
1595
+ data=in_sci2,
1596
+ exptime=1.0,
1597
+ pixmap=pixmap,
1598
+ weight_map=in_wht2,
1599
+ data2=in_sci2_sq,
1600
+ )
1601
+ assert str(err_info.value).startswith("Mismatch between the number of 'out_img2' images")
1602
+
1603
+ # 2 - test same number of data2 is used each time:
1604
+ driz = resample.Drizzle(
1605
+ kernel=kernel,
1606
+ )
1607
+
1608
+ driz.add_image(
1609
+ data=in_sci1,
1610
+ exptime=1.0,
1611
+ pixmap=pixmap,
1612
+ weight_map=in_wht1,
1613
+ data2=in_sci1_sq,
1614
+ )
1615
+
1616
+ with pytest.raises(ValueError) as err_info:
1617
+ driz.add_image(
1618
+ data=in_sci2,
1619
+ exptime=1.0,
1620
+ pixmap=pixmap,
1621
+ weight_map=in_wht2,
1622
+ data2=None,
1623
+ )
1624
+ assert str(err_info.value).startswith("Mismatch between the number of 'out_img2' images")
1625
+
1626
+ # 3 - test same number of data2 is used each time:
1627
+ driz = resample.Drizzle(
1628
+ kernel=kernel,
1629
+ out_img2=[out_img2, out_img2b],
1630
+ )
1631
+
1632
+ with pytest.raises(ValueError) as err_info:
1633
+ driz.add_image(
1634
+ data=in_sci2,
1635
+ exptime=1.0,
1636
+ pixmap=pixmap,
1637
+ weight_map=in_wht2,
1638
+ data2=in_sci1_sq,
1639
+ )
1640
+ assert str(err_info.value).startswith("Mismatch between the number of 'out_img2' images")
1641
+
1642
+ # 4 - test same number of data2 is used each time:
1643
+ driz = resample.Drizzle(
1644
+ kernel=kernel,
1645
+ out_img=out_img,
1646
+ out_img2=out_img2,
1647
+ )
1648
+
1649
+ with pytest.raises(ValueError) as err_info:
1650
+ driz.add_image(
1651
+ data=in_sci2,
1652
+ exptime=1.0,
1653
+ pixmap=pixmap,
1654
+ weight_map=in_wht2,
1655
+ data2=None,
1656
+ )
1657
+ assert str(err_info.value).startswith("Mismatch between the number of 'out_img2' images")
1658
+
1659
+ # 5 - test mismatch between output data image and output variance image:
1660
+ out_img2 = np.zeros(tuple(s + 1 for s in out_shape), dtype=np.float32)
1661
+
1662
+ with pytest.raises(ValueError) as err_info:
1663
+ driz = resample.Drizzle(
1664
+ kernel=kernel,
1665
+ out_img=out_img,
1666
+ out_img2=out_img2,
1667
+ )
1668
+ assert str(err_info.value).startswith("Inconsistent data shapes specified:")
1669
+
1670
+
1671
+ def test_drizzle_weights_squared_array_shape_mismatch():
1672
+ n = 20
1673
+ in_shape = (n, n)
1674
+ in_shape1 = (n + 1, n + 1)
1675
+ kernel = "square"
1676
+
1677
+ # input coordinate grid:
1678
+ y, x = np.indices(in_shape, dtype=np.float64)
1679
+
1680
+ in_sci1 = np.zeros(in_shape, dtype=np.float32)
1681
+ in_sci1[n // 2, n // 2] = 2.222222222222
1682
+ in_sci1_sq = np.zeros(in_shape, dtype=np.float32)
1683
+
1684
+ in_wht2 = np.zeros(in_shape1, dtype=np.float32)
1685
+ in_sci2_sq = np.zeros(in_shape1, dtype=np.float32)
1686
+
1687
+ pixmap = np.dstack([x, y])
1688
+
1689
+ out_shape = (int(y.max()) + 1, int(x.max()) + 1)
1690
+ out_shape1 = (out_shape[0] + 1, out_shape[1] + 1)
1691
+
1692
+ out_img2 = np.zeros(out_shape, dtype=np.float32)
1693
+ out_img2b = np.zeros(out_shape1, dtype=np.float32)
1694
+
1695
+ with pytest.raises(ValueError) as err_info:
1696
+ driz = resample.Drizzle(
1697
+ kernel=kernel,
1698
+ out_img2=[out_img2, out_img2b],
1699
+ )
1700
+ assert str(err_info.value).startswith("Inconsistent data shapes specified:")
1701
+
1702
+ driz = resample.Drizzle(
1703
+ kernel=kernel,
1704
+ out_img=out_img2.copy(),
1705
+ out_img2=[out_img2, out_img2, None],
1706
+ )
1707
+ with pytest.raises(ValueError) as err_info:
1708
+ driz.add_image(
1709
+ data=in_sci1,
1710
+ exptime=1.0,
1711
+ pixmap=pixmap,
1712
+ weight_map=in_wht2,
1713
+ data2=[in_sci1_sq, in_sci2_sq, None],
1714
+ )
1715
+ assert str(err_info.value).startswith("'data2' shape(s) is not consistent with 'data' shape.")
1716
+
1717
+ driz = resample.Drizzle(
1718
+ kernel=kernel,
1719
+ out_img2=out_img2,
1720
+ )
1721
+ with pytest.raises(ValueError) as err_info:
1722
+ driz.add_image(
1723
+ data=in_sci1,
1724
+ exptime=1.0,
1725
+ pixmap=pixmap,
1726
+ weight_map=in_wht2,
1727
+ data2=in_sci2_sq,
1728
+ )
1729
+ assert str(err_info.value).startswith("'data2' shape is not consistent with 'data' shape.")
1730
+
1731
+ with pytest.raises(ValueError) as err_info:
1732
+ driz = resample.Drizzle(
1733
+ kernel=kernel,
1734
+ out_img2=[out_img2, out_img2b],
1735
+ )
1736
+ assert str(err_info.value).startswith("Inconsistent data shapes specified:")
1737
+
1738
+ # wrong weight shape
1739
+ driz = resample.Drizzle(
1740
+ kernel=kernel,
1741
+ )
1742
+ with pytest.raises(ValueError) as err_info:
1743
+ driz.add_image(
1744
+ data=in_sci1,
1745
+ exptime=1.0,
1746
+ pixmap=pixmap,
1747
+ weight_map=in_wht2,
1748
+ )
1749
+ assert str(err_info.value).startswith("'weight_map' shape is not consistent with 'data' shape.")
1750
+
1751
+ # zero-sized variance array
1752
+ driz = resample.Drizzle(
1753
+ kernel=kernel, out_img2=[out_img2, out_img2.copy(), out_img2.copy(), None]
1754
+ )
1755
+ driz.add_image(
1756
+ data=in_sci1, exptime=1.0, pixmap=pixmap, data2=[in_sci1, in_sci1, np.array([]), None]
1757
+ )
1758
+ driz.add_image(data=in_sci1, exptime=1.0, pixmap=pixmap, data2=[in_sci1, None, in_sci1, None])
1759
+ assert np.allclose(np.nansum(driz.out_img2[0]), 2.0 * np.nansum(driz.out_img2[1]))
1760
+ assert np.allclose(np.nansum(driz.out_img2[0]), 2.0 * np.nansum(driz.out_img2[2]))
1761
+ assert np.allclose(0.0, np.nansum(driz.out_img2[3]))
1762
+
1763
+
1764
+ @pytest.mark.parametrize(
1765
+ "kernel_fc, pscale_ratio, kscale_none",
1766
+ (
1767
+ x
1768
+ for x in product(
1769
+ [
1770
+ ("square", True),
1771
+ ("point", True),
1772
+ ("turbo", True),
1773
+ ("lanczos2", False),
1774
+ ("lanczos3", False),
1775
+ ("gaussian", False),
1776
+ ],
1777
+ [0.9, 1.0, 1.2],
1778
+ [False, True],
1779
+ )
1780
+ ),
1781
+ )
1782
+ def test_drizzle_var_identical_to_nonvar(kernel_fc, pscale_ratio, kscale_none):
1783
+ """Test that the resampled science image using code with support for
1784
+ variance-propagation is identical to the resampled science image
1785
+ using code without support for variance-propagation (original code).
1786
+ """
1787
+ kernel, fc = kernel_fc
1788
+
1789
+ if kscale_none:
1790
+ kscale = None
1791
+ else:
1792
+ kscale = pscale_ratio
1793
+
1794
+ amplitude = 100.0
1795
+ inwcs = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
1796
+ insci = amplitude * np.random.random(inwcs.array_shape).astype(np.float32)
1797
+ inwht = np.ones_like(insci)
1798
+ output_wcs, _ = wcs_from_file("reference_square_image.fits", ext=1, return_data=True)
1799
+
1800
+ pixmap = utils.calc_pixmap(
1801
+ inwcs,
1802
+ output_wcs,
1803
+ )
1804
+
1805
+ driz1 = resample.Drizzle(
1806
+ kernel=kernel,
1807
+ fillval="NaN",
1808
+ out_shape=output_wcs.array_shape,
1809
+ exptime=0.0,
1810
+ begin_ctx_id=0,
1811
+ max_ctx_id=1,
1812
+ disable_ctx=False,
1813
+ )
1814
+
1815
+ driz2 = resample.Drizzle(
1816
+ kernel=kernel,
1817
+ fillval="NaN",
1818
+ out_shape=output_wcs.array_shape,
1819
+ exptime=0.0,
1820
+ begin_ctx_id=0,
1821
+ max_ctx_id=1,
1822
+ disable_ctx=False,
1823
+ )
1824
+
1825
+ if fc:
1826
+ driz1.add_image(
1827
+ insci,
1828
+ exptime=13.0,
1829
+ pixmap=pixmap,
1830
+ weight_map=inwht,
1831
+ iscale=pscale_ratio**2,
1832
+ pixel_scale_ratio=kscale,
1833
+ xmin=10,
1834
+ ymin=10,
1835
+ xmax=output_wcs.array_shape[0] - 10,
1836
+ ymax=output_wcs.array_shape[1] - 10,
1837
+ )
1838
+ driz2.add_image(
1839
+ insci,
1840
+ data2=insci,
1841
+ exptime=13.0,
1842
+ pixmap=pixmap,
1843
+ weight_map=inwht,
1844
+ iscale=pscale_ratio**2,
1845
+ pixel_scale_ratio=kscale,
1846
+ xmin=10,
1847
+ ymin=10,
1848
+ xmax=output_wcs.array_shape[0] - 10,
1849
+ ymax=output_wcs.array_shape[1] - 10,
1850
+ )
1851
+ else:
1852
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
1853
+ driz1.add_image(
1854
+ insci,
1855
+ exptime=13.0,
1856
+ pixmap=pixmap,
1857
+ weight_map=inwht,
1858
+ iscale=pscale_ratio**2,
1859
+ pixel_scale_ratio=kscale,
1860
+ xmin=10,
1861
+ ymin=10,
1862
+ xmax=output_wcs.array_shape[0] - 10,
1863
+ ymax=output_wcs.array_shape[1] - 10,
1864
+ )
1865
+
1866
+ with pytest.warns(Warning, match=f"Kernel '{kernel}' is not a flux-conserving kernel"):
1867
+ driz2.add_image(
1868
+ insci,
1869
+ data2=insci,
1870
+ exptime=13.0,
1871
+ pixmap=pixmap,
1872
+ weight_map=inwht,
1873
+ iscale=pscale_ratio**2,
1874
+ pixel_scale_ratio=kscale,
1875
+ xmin=10,
1876
+ ymin=10,
1877
+ xmax=output_wcs.array_shape[0] - 10,
1878
+ ymax=output_wcs.array_shape[1] - 10,
1879
+ )
1880
+
1881
+ assert np.allclose(
1882
+ driz1.out_img,
1883
+ driz2.out_img,
1884
+ rtol=0.0,
1885
+ atol=5.0 * amplitude * np.finfo(np.float32).eps,
1886
+ equal_nan=True,
1887
+ ), "Resampled science images are not identical."
1888
+
1889
+ assert np.allclose(
1890
+ driz1.out_wht,
1891
+ driz2.out_wht,
1892
+ rtol=0.0,
1893
+ atol=5.0 * amplitude * np.finfo(np.float32).eps,
1894
+ equal_nan=True,
1895
+ ), "Resampled weight images are not identical."
1896
+
1897
+ assert np.all(driz1.out_ctx == driz2.out_ctx), "Context images are not identical."
1898
+
1899
+
1900
+ @pytest.mark.parametrize("create_out_dq", [True, False])
1901
+ @pytest.mark.parametrize("shift", [0.0, 0.5])
1902
+ @pytest.mark.parametrize("add_non_dq_image", [True, False])
1903
+ def test_drizzle_dq_propagation(create_out_dq, shift, add_non_dq_image):
1904
+ n = 200
1905
+ in_shape = (n, n)
1906
+
1907
+ # input coordinate grid:
1908
+ y, x = np.indices(in_shape, dtype=np.float64) + shift
1909
+
1910
+ # simulate data:
1911
+ in_sci = np.ones(in_shape, dtype=np.float32)
1912
+ in_wht = np.ones(in_shape, dtype=np.float32)
1913
+ # use int16 to test up to 16 bits and signed integers
1914
+ in_dq1 = np.zeros(in_shape, dtype=np.int16)
1915
+ # use uint32 to test up to 32 bits and unsigned integers (default type)
1916
+ in_dq2 = np.zeros(in_shape, dtype=np.uint32)
1917
+ if create_out_dq:
1918
+ out_dq = np.zeros(in_shape, dtype=np.uint32)
1919
+ else:
1920
+ out_dq = None
1921
+
1922
+ xyc = n // 2
1923
+ in_dq1[xyc, xyc] = 1 << 0
1924
+ in_dq1[xyc + 1, xyc] = 1 << 1
1925
+ in_dq1[xyc, xyc + 1] = 1 << 2
1926
+ in_dq1[xyc + 1, xyc + 1] = 1 << 3
1927
+
1928
+ in_dq2[xyc, xyc] = 1 << 4
1929
+ in_dq2[xyc + 1, xyc] = 1 << 5
1930
+ in_dq2[xyc, xyc + 1] = 1 << 6
1931
+ in_dq2[xyc + 1, xyc + 1] = 1 << 7
1932
+
1933
+ driz = resample.Drizzle(
1934
+ kernel="square",
1935
+ out_dq=out_dq,
1936
+ )
1937
+
1938
+ pixmap = np.dstack([x, y])
1939
+
1940
+ driz.add_image(
1941
+ in_sci,
1942
+ dq=in_dq1,
1943
+ exptime=1.0,
1944
+ pixmap=pixmap,
1945
+ weight_map=in_wht,
1946
+ )
1947
+
1948
+ if shift == 0.0:
1949
+ assert driz.out_dq[xyc, xyc] == in_dq1[xyc, xyc]
1950
+ assert driz.out_dq[xyc + 1, xyc] == in_dq1[xyc + 1, xyc]
1951
+ assert driz.out_dq[xyc, xyc + 1] == in_dq1[xyc, xyc + 1]
1952
+ assert driz.out_dq[xyc + 1, xyc + 1] == in_dq1[xyc + 1, xyc + 1]
1953
+ else:
1954
+ # with shift=0.5 all 4 input pixels should contribute to the output
1955
+ # pixel at (xyc+1, xyc+1)
1956
+ assert driz.out_dq[xyc + 1, xyc + 1] == sum(1 << i for i in range(4))
1957
+
1958
+ if add_non_dq_image:
1959
+ driz.add_image(
1960
+ in_sci,
1961
+ exptime=1.0,
1962
+ pixmap=pixmap,
1963
+ weight_map=in_wht,
1964
+ )
1965
+
1966
+ if shift == 0.0:
1967
+ assert driz.out_dq[xyc, xyc] == in_dq1[xyc, xyc]
1968
+ assert driz.out_dq[xyc + 1, xyc] == in_dq1[xyc + 1, xyc]
1969
+ assert driz.out_dq[xyc, xyc + 1] == in_dq1[xyc, xyc + 1]
1970
+ assert driz.out_dq[xyc + 1, xyc + 1] == in_dq1[xyc + 1, xyc + 1]
1971
+ else:
1972
+ # with shift=0.5 all 4 input pixels should contribute to the output
1973
+ # pixel at (xyc+1, xyc+1)
1974
+ assert driz.out_dq[xyc + 1, xyc + 1] == sum(1 << i for i in range(4))
1975
+
1976
+ driz.add_image(
1977
+ in_sci,
1978
+ dq=in_dq2,
1979
+ exptime=1.0,
1980
+ pixmap=pixmap,
1981
+ weight_map=in_wht,
1982
+ )
1983
+
1984
+ in_dq_total = in_dq1 + in_dq2
1985
+
1986
+ if shift == 0.0:
1987
+ assert driz.out_dq[xyc, xyc] == in_dq_total[xyc, xyc]
1988
+ assert driz.out_dq[xyc + 1, xyc] == in_dq_total[xyc + 1, xyc]
1989
+ assert driz.out_dq[xyc, xyc + 1] == in_dq_total[xyc, xyc + 1]
1990
+ assert driz.out_dq[xyc + 1, xyc + 1] == in_dq_total[xyc + 1, xyc + 1]
1991
+ else:
1992
+ # with shift=0.5 all 4 input pixels should contribute to the output
1993
+ # pixel at (xyc+1, xyc+1)
1994
+ assert driz.out_dq[xyc + 1, xyc + 1] == sum(1 << i for i in range(8))
1995
+
1996
+ if add_non_dq_image:
1997
+ driz.add_image(
1998
+ in_sci,
1999
+ exptime=1.0,
2000
+ pixmap=pixmap,
2001
+ weight_map=in_wht,
2002
+ )
2003
+
2004
+ if shift == 0.0:
2005
+ assert driz.out_dq[xyc, xyc] == in_dq_total[xyc, xyc]
2006
+ assert driz.out_dq[xyc + 1, xyc] == in_dq_total[xyc + 1, xyc]
2007
+ assert driz.out_dq[xyc, xyc + 1] == in_dq_total[xyc, xyc + 1]
2008
+ assert driz.out_dq[xyc + 1, xyc + 1] == in_dq_total[xyc + 1, xyc + 1]
2009
+ else:
2010
+ # with shift=0.5 all 4 input pixels should contribute to the output
2011
+ # pixel at (xyc+1, xyc+1)
2012
+ assert driz.out_dq[xyc + 1, xyc + 1] == sum(1 << i for i in range(8))
2013
+
2014
+
2015
+ def test_drizzle_dq_propagation_wrong_shape():
2016
+ n = 200
2017
+ in_shape = (n, n)
2018
+
2019
+ # input coordinate grid:
2020
+ y, x = np.indices(in_shape, dtype=np.float64)
2021
+
2022
+ # simulate data:
2023
+ in_sci = np.ones(in_shape, dtype=np.float32)
2024
+ in_wht = np.ones(in_shape, dtype=np.float32)
2025
+ in_dq = np.zeros(tuple(i + 1 for i in in_shape), dtype=np.uint32)
2026
+ out_img = np.zeros(in_shape, dtype=np.float32)
2027
+ out_dq = np.zeros(tuple(i + 1 for i in in_shape), dtype=np.uint32)
2028
+
2029
+ with pytest.raises(ValueError) as err_info:
2030
+ driz = resample.Drizzle(
2031
+ kernel="square",
2032
+ out_img=out_img,
2033
+ out_dq=out_dq,
2034
+ )
2035
+ assert str(err_info.value).startswith("Inconsistent data shapes specified:")
2036
+
2037
+ driz = resample.Drizzle(
2038
+ kernel="square",
2039
+ )
2040
+
2041
+ pixmap = np.dstack([x, y])
2042
+
2043
+ with pytest.raises(ValueError) as err_info:
2044
+ driz.add_image(
2045
+ in_sci,
2046
+ dq=in_dq,
2047
+ exptime=1.0,
2048
+ pixmap=pixmap,
2049
+ weight_map=in_wht,
2050
+ )
2051
+ assert str(err_info.value).startswith("'dq' shape is not consistent with 'data' shape.")
2052
+
2053
+
2054
+ def test_drizzle_dq_propagation_wrong_type():
2055
+ n = 20
2056
+ in_shape = (n, n)
2057
+
2058
+ # input coordinate grid:
2059
+ y, x = np.indices(in_shape, dtype=np.float64)
2060
+
2061
+ # simulate data:
2062
+ in_sci = np.ones(in_shape, dtype=np.float32)
2063
+ in_wht = np.ones(in_shape, dtype=np.float32)
2064
+ in_dq = np.zeros(in_shape, dtype=np.uint64)
2065
+ out_img = np.zeros(in_shape, dtype=np.float32)
2066
+ out_dq = np.zeros(in_shape, dtype=np.uint64)
2067
+
2068
+ with pytest.raises(TypeError) as err_info:
2069
+ driz = resample.Drizzle(
2070
+ kernel="square",
2071
+ out_img=out_img,
2072
+ out_dq=out_dq,
2073
+ )
2074
+ assert str(err_info.value).startswith(
2075
+ "'out_dq' must be of an unsigned integer type with itemsize of 4 bytes or less"
2076
+ )
2077
+
2078
+ driz = resample.Drizzle(
2079
+ kernel="square",
2080
+ )
2081
+
2082
+ pixmap = np.dstack([x, y])
2083
+
2084
+ with pytest.raises(TypeError) as err_info:
2085
+ driz.add_image(
2086
+ in_sci,
2087
+ dq=in_dq,
2088
+ exptime=1.0,
2089
+ pixmap=pixmap,
2090
+ weight_map=in_wht,
2091
+ )
2092
+ assert str(err_info.value).startswith(
2093
+ "'dq' must be of an unsigned integer type with itemsize of 4 bytes or less."
2094
+ )
2095
+
2096
+
2097
+ @pytest.mark.parametrize(
2098
+ "kernel, pscale_ratio, use_var",
2099
+ (
2100
+ x
2101
+ for x in product(
2102
+ [
2103
+ "square",
2104
+ "point",
2105
+ "turbo",
2106
+ "lanczos2",
2107
+ "lanczos3",
2108
+ "gaussian",
2109
+ ],
2110
+ [0.9, 1.2, 0.3],
2111
+ [True, False],
2112
+ )
2113
+ ),
2114
+ )
2115
+ @pytest.mark.filterwarnings(r"ignore:Argument 'scale' has been deprecated.*:DeprecationWarning")
2116
+ @pytest.mark.filterwarnings(r"ignore:Kernel '.*' is not a flux-conserving kernel:Warning")
2117
+ def test_drizzle_ipscale_same_as_scale(kernel, pscale_ratio, use_var):
2118
+ """Test that the resampled science image using new "pixel_scale_ratio" and
2119
+ "iscale" parameters is identical to the resampled science image
2120
+ using the old "scale" parameter.
2121
+
2122
+ TODO: remove this test when support for "scale" is removed.
2123
+ """
2124
+ amplitude = 100.0
2125
+ inwcs = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
2126
+ insci = amplitude * np.random.random(inwcs.array_shape).astype(np.float32)
2127
+ inwht = np.ones_like(insci)
2128
+ output_wcs, _ = wcs_from_file("reference_square_image.fits", ext=1, return_data=True)
2129
+
2130
+ pixmap = utils.calc_pixmap(
2131
+ inwcs,
2132
+ output_wcs,
2133
+ )
2134
+
2135
+ driz1 = resample.Drizzle(
2136
+ kernel=kernel,
2137
+ fillval="NaN",
2138
+ out_shape=output_wcs.array_shape,
2139
+ exptime=0.0,
2140
+ begin_ctx_id=0,
2141
+ max_ctx_id=1,
2142
+ disable_ctx=False,
2143
+ )
2144
+
2145
+ driz2 = resample.Drizzle(
2146
+ kernel=kernel,
2147
+ fillval="NaN",
2148
+ out_shape=output_wcs.array_shape,
2149
+ exptime=0.0,
2150
+ begin_ctx_id=0,
2151
+ max_ctx_id=1,
2152
+ disable_ctx=False,
2153
+ )
2154
+
2155
+ driz1.add_image(
2156
+ insci,
2157
+ exptime=13.0,
2158
+ pixmap=pixmap,
2159
+ weight_map=inwht,
2160
+ iscale=pscale_ratio**2,
2161
+ pixel_scale_ratio=pscale_ratio,
2162
+ xmin=10,
2163
+ ymin=10,
2164
+ xmax=output_wcs.array_shape[0] - 10,
2165
+ ymax=output_wcs.array_shape[1] - 10,
2166
+ )
2167
+
2168
+ if use_var:
2169
+ kwargs = {"data2": insci}
2170
+ else:
2171
+ kwargs = {}
2172
+
2173
+ driz2.add_image(
2174
+ insci,
2175
+ exptime=13.0,
2176
+ pixmap=pixmap,
2177
+ weight_map=inwht,
2178
+ scale=pscale_ratio,
2179
+ xmin=10,
2180
+ ymin=10,
2181
+ xmax=output_wcs.array_shape[0] - 10,
2182
+ ymax=output_wcs.array_shape[1] - 10,
2183
+ **kwargs,
2184
+ )
2185
+
2186
+ assert np.allclose(
2187
+ driz1.out_img,
2188
+ driz2.out_img,
2189
+ rtol=0.0,
2190
+ atol=5.0 * amplitude * np.finfo(np.float32).eps,
2191
+ equal_nan=True,
2192
+ ), "Resampled science images are not identical."
2193
+
2194
+ assert np.allclose(
2195
+ driz1.out_wht,
2196
+ driz2.out_wht,
2197
+ rtol=0.0,
2198
+ atol=5.0 * amplitude * np.finfo(np.float32).eps,
2199
+ equal_nan=True,
2200
+ ), "Resampled weight images are not identical."
2201
+
2202
+ assert np.all(driz1.out_ctx == driz2.out_ctx), "Context images are not identical."