drizzle 2.1.1__cp314-cp314-macosx_11_0_arm64.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.

Potentially problematic release.


This version of drizzle might be problematic. Click here for more details.

@@ -0,0 +1,1350 @@
1
+ import math
2
+ import os
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ from astropy import wcs
8
+ from astropy.convolution import Gaussian2DKernel
9
+ from drizzle import cdrizzle, resample, utils
10
+
11
+ from .helpers import wcs_from_file
12
+
13
+ TEST_DIR = os.path.abspath(os.path.dirname(__file__))
14
+ DATA_DIR = os.path.join(TEST_DIR, 'data')
15
+
16
+
17
+ def bound_image(image):
18
+ """
19
+ Compute region where image is non-zero
20
+ """
21
+ coords = np.nonzero(image)
22
+ ymin = coords[0].min()
23
+ ymax = coords[0].max()
24
+ xmin = coords[1].min()
25
+ xmax = coords[1].max()
26
+ return (ymin, ymax, xmin, xmax)
27
+
28
+
29
+ def centroid(image, size, center):
30
+ """
31
+ Compute the centroid of a rectangular area
32
+ """
33
+ ylo = int(center[0] - size / 2)
34
+ yhi = min(ylo + size, image.shape[0])
35
+ xlo = int(center[1] - size / 2)
36
+ xhi = min(xlo + size, image.shape[1])
37
+
38
+ yx1 = np.mgrid[ylo:yhi, xlo:xhi, 1:2]
39
+ center = (yx1[..., 0] * image[ylo:yhi, xlo:xhi]).sum(
40
+ axis=(1, 2),
41
+ dtype=np.float64,
42
+ )
43
+
44
+ if center[2] == 0.0:
45
+ return None
46
+
47
+ center[0] /= center[2]
48
+ center[1] /= center[2]
49
+ return center
50
+
51
+
52
+ def centroid_close(list_of_centroids, size, point):
53
+ """
54
+ Find if any centroid is close to a point
55
+ """
56
+ for i in range(len(list_of_centroids) - 1, -1, -1):
57
+ if (abs(list_of_centroids[i][0] - point[0]) < int(size / 2) and
58
+ abs(list_of_centroids[i][1] - point[1]) < int(size / 2)):
59
+ return 1
60
+
61
+ return 0
62
+
63
+
64
+ def centroid_compare(centroid):
65
+ return centroid[1]
66
+
67
+
68
+ def centroid_distances(image1, image2, amp, size):
69
+ """
70
+ Compute a list of centroids and the distances between them in two images
71
+ """
72
+ distances = []
73
+ list_of_centroids = centroid_list(image2, amp, size)
74
+ for center2 in list_of_centroids:
75
+ center1 = centroid(image1, size, center2)
76
+ if center1 is None:
77
+ continue
78
+
79
+ disty = center2[0] - center1[0]
80
+ distx = center2[1] - center1[1]
81
+ dist = math.sqrt(disty * disty + distx * distx)
82
+ dflux = abs(center2[2] - center1[2])
83
+ distances.append([dist, dflux, center1, center2])
84
+
85
+ distances.sort(key=centroid_compare)
86
+ return distances
87
+
88
+
89
+ def centroid_list(image, amp, size):
90
+ """
91
+ Find the next centroid
92
+ """
93
+ list_of_centroids = []
94
+ points = np.transpose(np.nonzero(image > amp))
95
+ for point in points:
96
+ if not centroid_close(list_of_centroids, size, point):
97
+ center = centroid(image, size, point)
98
+ list_of_centroids.append(center)
99
+
100
+ return list_of_centroids
101
+
102
+
103
+ def centroid_statistics(title, fname, image1, image2, amp, size):
104
+ """
105
+ write centroid statistics to compare differences btw two images
106
+ """
107
+ stats = ("minimum", "median", "maximum")
108
+ images = (None, None, image1, image2)
109
+ im_type = ("", "", "test", "reference")
110
+
111
+ diff = []
112
+ distances = centroid_distances(image1, image2, amp, size)
113
+ indexes = (0, int(len(distances) / 2), len(distances) - 1)
114
+ fd = open(fname, 'w')
115
+ fd.write(f"*** {title:s} ***\n")
116
+
117
+ if len(distances) == 0:
118
+ diff = [0.0, 0.0, 0.0]
119
+ fd.write("No matches!!\n")
120
+
121
+ elif len(distances) == 1:
122
+ diff = [distances[0][0], distances[0][0], distances[0][0]]
123
+
124
+ fd.write("1 match\n")
125
+ fd.write(
126
+ f"distance = {distances[0][0]:f} "
127
+ f"flux difference = {distances[0][1]:f}\n"
128
+ )
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
+ np.random.seed(0)
204
+
205
+ patch_size = 21
206
+ p2 = patch_size // 2
207
+ # add border so that resampled partial pixels can be isolated
208
+ # in the segmentation:
209
+ border = 4
210
+ pwb = patch_size + border
211
+
212
+ fwhm2sigma = 2.0 * math.sqrt(2.0 * math.log(2.0))
213
+
214
+ ny, nx = data.shape
215
+
216
+ stars = []
217
+
218
+ for yc in range(border + p2, ny - pwb, pwb):
219
+ for xc in range(border + p2, nx - pwb, pwb):
220
+ sl = np.s_[yc - p2:yc + p2 + 1, xc - p2:xc + p2 + 1]
221
+ flux = 1.0 + 99.0 * np.random.random()
222
+ if np.random.random() > 0.7:
223
+ # uniform image
224
+ psf = np.full((patch_size, patch_size), flux)
225
+ else:
226
+ # "star":
227
+ fwhm = 1.5 + 1.5 * np.random.random()
228
+ sigma = fwhm / fwhm2sigma
229
+
230
+ psf = flux * Gaussian2DKernel(
231
+ sigma,
232
+ x_size=patch_size,
233
+ y_size=patch_size
234
+ ).array
235
+ weight = 0.6 + 0.4 * np.random.random((patch_size, patch_size))
236
+ wflux = (psf * weight).sum()
237
+
238
+ data[sl] = psf
239
+ wht[sl] = weight
240
+ dq[sl] = 0
241
+ stars.append((xc, yc, wflux, sl))
242
+
243
+ return data, wht, dq, stars, wcs
244
+
245
+
246
+ def test_drizzle_defaults():
247
+ n = 200
248
+ in_shape = (n, n)
249
+
250
+ # input coordinate grid:
251
+ y, x = np.indices(in_shape, dtype=np.float64)
252
+
253
+ # simulate data:
254
+ in_sci = np.ones(in_shape, dtype=np.float32)
255
+ in_wht = np.ones(in_shape, dtype=np.float32)
256
+
257
+ # create a Drizzle object using all default parameters (except for 'kernel')
258
+ driz = resample.Drizzle(
259
+ kernel='square',
260
+ )
261
+
262
+ assert driz.out_img is None
263
+ assert driz.out_wht is None
264
+ assert driz.out_ctx is None
265
+ assert driz.total_exptime == 0.0
266
+
267
+ driz.add_image(
268
+ in_sci,
269
+ exptime=1.0,
270
+ pixmap=np.dstack([x, y]),
271
+ weight_map=in_wht,
272
+ )
273
+
274
+ pixmap = np.dstack([x + 1, y + 2])
275
+ driz.add_image(
276
+ 3 * in_sci,
277
+ exptime=1.0,
278
+ pixmap=pixmap,
279
+ weight_map=in_wht,
280
+ )
281
+
282
+ assert driz.out_img[0, 0] == 1
283
+ assert driz.out_img[1, 0] == 1
284
+ assert driz.out_img[2, 0] == 1
285
+ assert driz.out_img[1, 1] == 1
286
+ assert driz.out_img[1, 2] == 1
287
+ assert (driz.out_img[2, 1] - 2.0) < 1.0e-14
288
+
289
+
290
+ @pytest.mark.parametrize(
291
+ 'kernel,test_image_type,max_diff_atol',
292
+ [
293
+ ("square", "point", 1.0e-5),
294
+ ("square", "grid", 1.0e-5),
295
+ ('point', "grid", 1.0e-5),
296
+ ("turbo", "grid", 1.0e-5),
297
+ ('lanczos3', "grid", 1.0e-5),
298
+ ("gaussian", "grid", 2.0e-5),
299
+ ],
300
+ )
301
+ def test_resample_kernel(tmpdir, kernel, test_image_type, max_diff_atol):
302
+ """
303
+ Test do_driz square kernel with point
304
+ """
305
+ output_difference = str(
306
+ tmpdir.join(f"difference_{kernel}_{test_image_type}.txt")
307
+ )
308
+
309
+ inwcs = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
310
+ if test_image_type == "point":
311
+ insci = make_point_image(inwcs.array_shape, (500, 200), 100.0)
312
+ else:
313
+ insci = make_grid_image(inwcs.array_shape, 64, 100.0)
314
+ inwht = np.ones_like(insci)
315
+ output_wcs, template_data = wcs_from_file(
316
+ f"reference_{kernel}_{test_image_type}.fits",
317
+ ext=1,
318
+ 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] /
341
+ np.sum(inwcs.wcs.cd**2, axis=0)[0]
342
+ )
343
+
344
+ driz = resample.Drizzle(
345
+ kernel=kernel,
346
+ out_shape=output_wcs.array_shape,
347
+ fillval=0.0,
348
+ )
349
+
350
+ if kernel in ["square", "turbo", "point"]:
351
+ driz.add_image(
352
+ insci,
353
+ exptime=1.0,
354
+ pixmap=pixmap,
355
+ weight_map=inwht,
356
+ scale=pscale_ratio,
357
+ )
358
+ else:
359
+ with pytest.warns(
360
+ Warning,
361
+ match=f"Kernel '{kernel}' is not a flux-conserving kernel"
362
+ ):
363
+ driz.add_image(
364
+ insci,
365
+ exptime=1.0,
366
+ pixmap=pixmap,
367
+ weight_map=inwht,
368
+ scale=pscale_ratio,
369
+ )
370
+
371
+ _, med_diff, max_diff = centroid_statistics(
372
+ f"{kernel} with {test_image_type}",
373
+ output_difference,
374
+ driz.out_img,
375
+ template_data,
376
+ 20.0,
377
+ 8,
378
+ )
379
+
380
+ assert med_diff < 1.0e-6
381
+ assert max_diff < max_diff_atol
382
+
383
+
384
+ @pytest.mark.parametrize(
385
+ 'kernel,max_diff_atol',
386
+ [
387
+ ("square", 1.0e-5),
388
+ ("turbo", 1.0e-5),
389
+ ],
390
+ )
391
+ def test_resample_kernel_image(tmpdir, kernel, max_diff_atol):
392
+ """
393
+ Test do_driz square kernel with point
394
+ """
395
+ inwcs, insci = wcs_from_file(
396
+ "j8bt06nyq_flt.fits",
397
+ ext=1,
398
+ return_data=True
399
+ )
400
+ inwht = np.ones_like(insci)
401
+
402
+ outwcs, ref_sci, ref_ctx, ref_wht = wcs_from_file(
403
+ f"reference_{kernel}_image.fits",
404
+ ext=1,
405
+ return_data=["SCI", "CTX", "WHT"]
406
+ )
407
+ ref_ctx = np.array(ref_ctx, dtype=np.int32)
408
+
409
+ pixmap = utils.calc_pixmap(
410
+ inwcs,
411
+ outwcs,
412
+ )
413
+
414
+ pscale_ratio = np.sqrt(
415
+ np.sum(outwcs.wcs.cd**2, axis=0)[0] /
416
+ np.sum(inwcs.wcs.cd**2, axis=0)[0]
417
+ )
418
+
419
+ driz = resample.Drizzle(
420
+ kernel=kernel,
421
+ out_shape=ref_sci.shape,
422
+ fillval=0.0,
423
+ )
424
+
425
+ driz.add_image(
426
+ insci,
427
+ exptime=1.0,
428
+ pixmap=pixmap,
429
+ weight_map=inwht,
430
+ scale=pscale_ratio,
431
+ )
432
+ outctx = driz.out_ctx[0]
433
+
434
+ # in order to avoid small differences in the staircase in the outline
435
+ # of the input image in the output grid, select a subset:
436
+ sl = np.s_[125: -125, 5: -5]
437
+
438
+ assert np.allclose(driz.out_img[sl], ref_sci[sl], atol=0, rtol=1.0e-6)
439
+ assert np.allclose(driz.out_wht[sl], ref_wht[sl], atol=0, rtol=1.0e-6)
440
+ assert np.all(outctx[sl] == ref_ctx[sl])
441
+
442
+
443
+ @pytest.mark.parametrize(
444
+ 'kernel,fc',
445
+ [
446
+ ('square', True),
447
+ ('point', True),
448
+ ('turbo', True),
449
+ ('lanczos2', False),
450
+ ('lanczos3', False),
451
+ ('gaussian', False),
452
+ ],
453
+ )
454
+ def test_zero_input_weight(kernel, fc):
455
+ """
456
+ Test do_driz square kernel with grid
457
+ """
458
+ # initialize input:
459
+ insci = np.ones((200, 400), dtype=np.float32)
460
+ inwht = np.ones((200, 400), dtype=np.float32)
461
+ inwht[:, 150:155] = 0
462
+
463
+ # initialize output:
464
+ outsci = np.zeros((210, 410), dtype=np.float32)
465
+ outwht = np.zeros((210, 410), dtype=np.float32)
466
+ outctx = np.zeros((210, 410), dtype=np.int32)
467
+
468
+ # define coordinate mapping:
469
+ pixmap = np.moveaxis(np.mgrid[1:201, 1:401][::-1], 0, -1)
470
+
471
+ # resample:
472
+ if fc:
473
+ cdrizzle.tdriz(
474
+ insci,
475
+ inwht,
476
+ pixmap,
477
+ outsci,
478
+ outwht,
479
+ outctx,
480
+ uniqid=1,
481
+ xmin=0,
482
+ xmax=400,
483
+ ymin=0,
484
+ ymax=200,
485
+ pixfrac=1,
486
+ kernel=kernel,
487
+ in_units='cps',
488
+ expscale=1,
489
+ wtscale=1,
490
+ fillstr='INDEF',
491
+ )
492
+ else:
493
+ with pytest.warns(
494
+ Warning,
495
+ match=f"Kernel '{kernel}' is not a flux-conserving kernel"
496
+ ):
497
+ cdrizzle.tdriz(
498
+ insci,
499
+ inwht,
500
+ pixmap,
501
+ outsci,
502
+ outwht,
503
+ outctx,
504
+ uniqid=1,
505
+ xmin=0,
506
+ xmax=400,
507
+ ymin=0,
508
+ ymax=200,
509
+ pixfrac=1,
510
+ kernel=kernel,
511
+ in_units='cps',
512
+ expscale=1,
513
+ wtscale=1,
514
+ fillstr='INDEF',
515
+ )
516
+ # pytest.xfail("Not a flux-conserving kernel")
517
+
518
+ # check that no pixel with 0 weight has any counts:
519
+ assert np.sum(np.abs(outsci[(outwht == 0)])) == 0.0
520
+
521
+
522
+ @pytest.mark.parametrize(
523
+ 'interpolator,test_image_type',
524
+ [
525
+ ("poly5", "point"),
526
+ ("default", "grid"),
527
+ ('lan3', "grid"),
528
+ ("lan5", "grid"),
529
+ ],
530
+ )
531
+ def test_blot_interpolation(tmpdir, interpolator, test_image_type):
532
+ """
533
+ Test do_driz square kernel with point
534
+ """
535
+ output_difference = str(
536
+ tmpdir.join(f"difference_blot_{interpolator}_{test_image_type}.txt")
537
+ )
538
+
539
+ outwcs = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
540
+ if test_image_type == "point":
541
+ outsci = make_point_image(outwcs.array_shape, (500, 200), 40.0)
542
+ ref_fname = "reference_blot_point.fits"
543
+ else:
544
+ outsci = make_grid_image(outwcs.array_shape, 64, 100.0)
545
+ ref_fname = f"reference_blot_{interpolator}.fits"
546
+ inwcs, template_data = wcs_from_file(ref_fname, ext=1, return_data=True)
547
+
548
+ pixmap = utils.calc_pixmap(inwcs, outwcs)
549
+
550
+ # compute pscale the old way (only to make
551
+ # tests work with old truth files and thus to show that new API gives
552
+ # same results when equal definitions of the pixel scale is used):
553
+ pscale_ratio = np.sqrt(
554
+ np.sum(inwcs.wcs.pc**2, axis=0)[0] /
555
+ np.sum(outwcs.wcs.cd**2, axis=0)[0]
556
+ )
557
+
558
+ if interpolator == "default":
559
+ kwargs = {}
560
+ else:
561
+ kwargs = {"interp": interpolator}
562
+
563
+ blotted_image = resample.blot_image(
564
+ outsci,
565
+ pixmap=pixmap,
566
+ pix_ratio=pscale_ratio,
567
+ exptime=1.0,
568
+ output_pixel_shape=inwcs.pixel_shape,
569
+ **kwargs
570
+ )
571
+
572
+ _, med_diff, max_diff = centroid_statistics(
573
+ "blot with '{interpolator}' and '{test_image_type}'",
574
+ output_difference,
575
+ blotted_image,
576
+ template_data,
577
+ 20.0,
578
+ 16,
579
+ )
580
+ assert med_diff < 1.0e-6
581
+ assert max_diff < 1.0e-5
582
+
583
+
584
+ def test_context_planes():
585
+ """Reproduce error seen in issue #50"""
586
+ shape = (10, 10)
587
+ output_wcs = wcs.WCS()
588
+ output_wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN']
589
+ output_wcs.wcs.pc = [[1, 0], [0, 1]]
590
+ output_wcs.pixel_shape = shape
591
+ driz = resample.Drizzle(out_shape=tuple(shape))
592
+
593
+ image = np.ones(shape)
594
+ inwcs = wcs.WCS()
595
+ inwcs.wcs.ctype = ['RA---TAN', 'DEC--TAN']
596
+ inwcs.wcs.cd = [[1, 0], [0, 1]]
597
+ inwcs.pixel_shape = shape
598
+
599
+ pixmap = utils.calc_pixmap(inwcs, output_wcs)
600
+
601
+ # context image must be 2D or 3D:
602
+ with pytest.raises(ValueError) as err_info:
603
+ resample.Drizzle(
604
+ kernel='point',
605
+ exptime=0.0,
606
+ out_shape=shape,
607
+ out_ctx=[0, 0, 0],
608
+ )
609
+ assert str(err_info.value).startswith(
610
+ "'out_ctx' must be either a 2D or 3D array."
611
+ )
612
+
613
+ driz = resample.Drizzle(
614
+ kernel='square',
615
+ out_shape=output_wcs.array_shape,
616
+ fillval=0.0,
617
+ )
618
+
619
+ for i in range(32):
620
+ assert driz.ctx_id == i
621
+ driz.add_image(image, exptime=1.0, pixmap=pixmap)
622
+ assert driz.out_ctx.shape == (1, 10, 10)
623
+
624
+ driz.add_image(image, exptime=1.0, pixmap=pixmap)
625
+ assert driz.out_ctx.shape == (2, 10, 10)
626
+
627
+
628
+ def test_no_context_image():
629
+ """Reproduce error seen in issue #50"""
630
+ shape = (10, 10)
631
+ output_wcs = wcs.WCS()
632
+ output_wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN']
633
+ output_wcs.wcs.pc = [[1, 0], [0, 1]]
634
+ output_wcs.pixel_shape = shape
635
+ driz = resample.Drizzle(
636
+ out_shape=tuple(shape),
637
+ begin_ctx_id=-1,
638
+ disable_ctx=True
639
+ )
640
+ assert driz.out_ctx is None
641
+ assert driz.ctx_id is None
642
+
643
+ image = np.ones(shape)
644
+ inwcs = wcs.WCS()
645
+ inwcs.wcs.ctype = ['RA---TAN', 'DEC--TAN']
646
+ inwcs.wcs.cd = [[1, 0], [0, 1]]
647
+ inwcs.pixel_shape = shape
648
+
649
+ pixmap = utils.calc_pixmap(inwcs, output_wcs)
650
+
651
+ for i in range(33):
652
+ driz.add_image(image, exptime=1.0, pixmap=pixmap)
653
+ assert driz.out_ctx is None
654
+ assert driz.ctx_id is None
655
+
656
+
657
+ def test_init_ctx_id():
658
+ # starting context ID must be positive
659
+ with pytest.raises(ValueError) as err_info:
660
+ resample.Drizzle(
661
+ kernel='square',
662
+ exptime=0.0,
663
+ begin_ctx_id=-1,
664
+ out_shape=(10, 10),
665
+ )
666
+ assert str(err_info.value).startswith(
667
+ "Invalid context image ID"
668
+ )
669
+
670
+ with pytest.raises(ValueError) as err_info:
671
+ resample.Drizzle(
672
+ kernel='square',
673
+ exptime=0.0,
674
+ out_shape=(10, 10),
675
+ begin_ctx_id=1,
676
+ max_ctx_id=0,
677
+ )
678
+ assert str(err_info.value).startswith(
679
+ "'max_ctx_id' cannot be smaller than 'begin_ctx_id'."
680
+ )
681
+
682
+
683
+ def test_context_agrees_with_weight():
684
+ n = 200
685
+ out_shape = (n, n)
686
+
687
+ # allocate output arrays:
688
+ out_img = np.zeros(out_shape, dtype=np.float32)
689
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
690
+ out_wht = np.zeros(out_shape, dtype=np.float32)
691
+
692
+ # previous data in weight and context must agree:
693
+ with pytest.raises(ValueError) as err_info:
694
+ out_ctx[0, 0] = 1
695
+ out_ctx[0, 1] = 1
696
+ out_wht[0, 0] = 0.1
697
+ resample.Drizzle(
698
+ kernel='square',
699
+ out_shape=out_shape,
700
+ out_img=out_img,
701
+ out_ctx=out_ctx,
702
+ out_wht=out_wht,
703
+ exptime=1.0,
704
+ )
705
+ assert str(err_info.value).startswith(
706
+ "Inconsistent values of supplied 'out_wht' and 'out_ctx' "
707
+ )
708
+
709
+
710
+ @pytest.mark.parametrize(
711
+ 'kernel,fc',
712
+ [
713
+ ('square', True),
714
+ ('point', True),
715
+ ('turbo', True),
716
+ ('lanczos2', False),
717
+ ('lanczos3', False),
718
+ ('gaussian', False),
719
+ ],
720
+ )
721
+ def test_flux_conservation_nondistorted(kernel, fc):
722
+ n = 200
723
+ in_shape = (n, n)
724
+
725
+ # input coordinate grid:
726
+ y, x = np.indices(in_shape, dtype=np.float64)
727
+
728
+ # simulate a gaussian "star":
729
+ fwhm = 2.9
730
+ x0 = 50.0
731
+ y0 = 68.0
732
+ sig = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0 * fwhm)))
733
+ sig2 = sig * sig
734
+ star = np.exp(-0.5 / sig2 * ((x.astype(np.float32) - x0)**2 + (y.astype(np.float32) - y0)**2))
735
+ in_sci = (star / np.sum(star)).astype(np.float32) # normalize to 1
736
+ in_wht = np.ones(in_shape, dtype=np.float32)
737
+
738
+ # linear shift:
739
+ xp = x + 0.5
740
+ yp = y + 0.2
741
+
742
+ pixmap = np.dstack([xp, yp])
743
+
744
+ out_shape = (int(yp.max()) + 1, int(xp.max()) + 1)
745
+ # make sure distorion is not moving flux out of the image towards negative
746
+ # coordinates (just because of the simple way of how we account for output
747
+ # image size)
748
+ assert np.min(xp) > -0.5 and np.min(yp) > -0.5
749
+
750
+ out_img = np.zeros(out_shape, dtype=np.float32)
751
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
752
+ out_wht = np.zeros(out_shape, dtype=np.float32)
753
+
754
+ if fc:
755
+ cdrizzle.tdriz(
756
+ in_sci,
757
+ in_wht,
758
+ pixmap,
759
+ out_img,
760
+ out_wht,
761
+ out_ctx,
762
+ pixfrac=1.0,
763
+ scale=1.0,
764
+ kernel=kernel,
765
+ in_units="cps",
766
+ expscale=1.0,
767
+ wtscale=1.0,
768
+ )
769
+ else:
770
+ with pytest.warns(
771
+ Warning,
772
+ match=f"Kernel '{kernel}' is not a flux-conserving kernel"
773
+ ):
774
+ cdrizzle.tdriz(
775
+ in_sci,
776
+ in_wht,
777
+ pixmap,
778
+ out_img,
779
+ out_wht,
780
+ out_ctx,
781
+ pixfrac=1.0,
782
+ scale=1.0,
783
+ kernel=kernel,
784
+ in_units="cps",
785
+ expscale=1.0,
786
+ wtscale=1.0,
787
+ )
788
+ pytest.xfail("Not a flux-conserving kernel")
789
+
790
+ assert np.allclose(
791
+ np.sum(out_img * out_wht),
792
+ np.sum(in_sci),
793
+ atol=0.0,
794
+ rtol=0.0001,
795
+ )
796
+
797
+
798
+ @pytest.mark.parametrize(
799
+ 'kernel,fc',
800
+ [
801
+ ('square', True),
802
+ ('point', True),
803
+ ('turbo', True),
804
+ ('lanczos2', False),
805
+ ('lanczos3', False),
806
+ ('gaussian', False),
807
+ ],
808
+ )
809
+ def test_flux_conservation_distorted(kernel, fc):
810
+ n = 200
811
+ in_shape = (n, n)
812
+
813
+ # input coordinate grid:
814
+ y, x = np.indices(in_shape, dtype=np.float64)
815
+
816
+ # simulate a gaussian "star":
817
+ fwhm = 2.9
818
+ x0 = 50.0
819
+ y0 = 68.0
820
+ sig = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0 * fwhm)))
821
+ sig2 = sig * sig
822
+ star = np.exp(-0.5 / sig2 * ((x.astype(np.float32) - x0)**2 + (y.astype(np.float32) - y0)**2))
823
+ in_sci = (star / np.sum(star)).astype(np.float32) # normalize to 1
824
+ in_wht = np.ones(in_shape, dtype=np.float32)
825
+
826
+ # linear shift:
827
+ xp = x + 0.5
828
+ yp = y + 0.2
829
+ # add distortion:
830
+ xp += 1e-4 * x**2 + 1e-5 * x * y
831
+ yp += 1e-3 * y**2 - 2e-5 * x * y
832
+
833
+ pixmap = np.dstack([xp, yp])
834
+
835
+ out_shape = (int(yp.max()) + 1, int(xp.max()) + 1)
836
+ # make sure distorion is not moving (pixels with) flux out of the image
837
+ # towards negative coordinates (just because of the simple way of how we
838
+ # account for output image size):
839
+ assert np.min(xp) > -0.5 and np.min(yp) > -0.5
840
+
841
+ out_img = np.zeros(out_shape, dtype=np.float32)
842
+ out_ctx = np.zeros(out_shape, dtype=np.int32)
843
+ out_wht = np.zeros(out_shape, dtype=np.float32)
844
+
845
+ if fc:
846
+ cdrizzle.tdriz(
847
+ in_sci,
848
+ in_wht,
849
+ pixmap,
850
+ out_img,
851
+ out_wht,
852
+ out_ctx,
853
+ pixfrac=1.0,
854
+ scale=1.0,
855
+ kernel=kernel,
856
+ in_units="cps",
857
+ expscale=1.0,
858
+ wtscale=1.0,
859
+ )
860
+ else:
861
+ with pytest.warns(
862
+ Warning,
863
+ match=f"Kernel '{kernel}' is not a flux-conserving kernel"
864
+ ):
865
+ cdrizzle.tdriz(
866
+ in_sci,
867
+ in_wht,
868
+ pixmap,
869
+ out_img,
870
+ out_wht,
871
+ out_ctx,
872
+ pixfrac=1.0,
873
+ scale=1.0,
874
+ kernel=kernel,
875
+ in_units="cps",
876
+ expscale=1.0,
877
+ wtscale=1.0,
878
+ )
879
+ pytest.xfail("Not a flux-conserving kernel")
880
+
881
+ assert np.allclose(
882
+ np.sum(out_img * out_wht),
883
+ np.sum(in_sci),
884
+ atol=0.0,
885
+ rtol=0.0001,
886
+ )
887
+
888
+
889
+ @pytest.mark.parametrize("kernel", ["square", "turbo", "point"])
890
+ @pytest.mark.parametrize("pscale_ratio", [0.55, 1.0, 1.2])
891
+ def test_flux_conservation_distorted_distributed_sources(nrcb5_stars, kernel, pscale_ratio):
892
+ """ test aperture photometry """
893
+ insci, inwht, dq, stars, wcs = nrcb5_stars
894
+
895
+ suffix = f"{pscale_ratio}".replace(".", "p")
896
+ output_wcs = wcs_from_file(f"nrcb5_output_wcs_psr_{suffix}.hdr")
897
+
898
+ pixmap = utils.calc_pixmap(
899
+ wcs,
900
+ output_wcs,
901
+ wcs.array_shape,
902
+ )
903
+
904
+ driz = resample.Drizzle(
905
+ kernel=kernel,
906
+ out_shape=output_wcs.array_shape,
907
+ fillval=0.0,
908
+ )
909
+ driz.add_image(
910
+ insci,
911
+ exptime=1.0,
912
+ pixmap=pixmap,
913
+ weight_map=inwht,
914
+ scale=1,
915
+ )
916
+
917
+ # for efficiency, instead of doing this patch-by-patch,
918
+ # multiply resampled data by resampled image weight
919
+ out_data = driz.out_img * driz.out_wht
920
+
921
+ dim3 = (slice(None, None, None), )
922
+ for _, _, fin, sl in stars:
923
+ xyout = pixmap[sl + dim3]
924
+ xmin = int(np.floor(xyout[:, :, 0].min() - 0.5))
925
+ xmax = int(np.ceil(xyout[:, :, 0].max() + 1.5))
926
+ ymin = int(np.floor(xyout[:, :, 1].min() - 0.5))
927
+ ymax = int(np.ceil(xyout[:, :, 1].max() + 1.5))
928
+ fout = np.nansum(out_data[ymin:ymax, xmin:xmax])
929
+
930
+ assert np.allclose(fin, fout, 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(
1007
+ "Exposure time must be 0.0 for the first resampling"
1008
+ )
1009
+
1010
+
1011
+ def test_drizzle_unsupported_kernel():
1012
+ with pytest.raises(ValueError) as err_info:
1013
+ resample.Drizzle(
1014
+ kernel='magic_image_improver',
1015
+ out_shape=(10, 10),
1016
+ exptime=0.0,
1017
+ )
1018
+ assert str(err_info.value) == "Kernel 'magic_image_improver' is not supported."
1019
+
1020
+
1021
+ def test_pixmap_shape_matches_image():
1022
+ n = 200
1023
+ in_shape = (n, n)
1024
+
1025
+ # input coordinate grid:
1026
+ y, x = np.indices((n + 1, n), dtype=np.float64)
1027
+
1028
+ # simulate data:
1029
+ in_sci = np.ones(in_shape, dtype=np.float32)
1030
+ in_wht = np.ones(in_shape, dtype=np.float32)
1031
+
1032
+ pixmap = np.dstack([x, y])
1033
+
1034
+ driz = resample.Drizzle(
1035
+ kernel='square',
1036
+ fillval=0.0,
1037
+ exptime=0.0,
1038
+ )
1039
+
1040
+ # last two sizes of the pixelmap must match those of input images:
1041
+ with pytest.raises(ValueError) as err_info:
1042
+ driz.add_image(
1043
+ in_sci,
1044
+ exptime=1.0,
1045
+ pixmap=pixmap,
1046
+ weight_map=in_wht,
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(-0.5 / sig2 * ((x.astype(np.float32) - x0)**2 + (y.astype(np.float32) - y0)**2))
1065
+ in_sci = (star / np.sum(star)).astype(np.float32) # normalize to 1
1066
+ in_wht = np.zeros(in_shape, dtype=np.float32)
1067
+ mask = np.where((x.astype(np.float32) - x0)**2 + (y.astype(np.float32) - y0)**2 <= 10)
1068
+ in_wht[mask] = 1.0
1069
+
1070
+ # linear shift:
1071
+ xp = x + 50
1072
+ yp = y + 50
1073
+
1074
+ pixmap = np.dstack([xp, yp])
1075
+
1076
+ out_shape = (int(yp.max()) + 1, int(xp.max()) + 1)
1077
+ # make sure distorion is not moving flux out of the image towards negative
1078
+ # coordinates (just because of the simple way of how we account for output
1079
+ # image size)
1080
+ assert np.min(xp) > -0.5 and np.min(yp) > -0.5
1081
+
1082
+ out_img = np.zeros(out_shape, dtype=np.float32) - 1.11
1083
+ out_ctx = np.zeros((1, ) + out_shape, dtype=np.int32)
1084
+ out_wht = np.zeros(out_shape, dtype=np.float32)
1085
+
1086
+ driz = resample.Drizzle(
1087
+ kernel='square',
1088
+ out_shape=out_shape,
1089
+ fillval="indef",
1090
+ exptime=0.0,
1091
+ )
1092
+
1093
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap)
1094
+ assert np.isnan(driz.out_img[0, 0])
1095
+ assert driz.out_img[int(y0) + 50, int(x0) + 50] > 0.0
1096
+
1097
+ driz = resample.Drizzle(
1098
+ kernel='square',
1099
+ out_shape=out_shape,
1100
+ fillval="-1.11",
1101
+ out_img=out_img.copy(),
1102
+ out_ctx=out_ctx.copy(),
1103
+ out_wht=out_wht.copy(),
1104
+ exptime=0.0,
1105
+ )
1106
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap)
1107
+ assert np.allclose(driz.out_img[0, 0], -1.11, rtol=0.0, atol=1.0e-7)
1108
+ assert driz.out_img[int(y0) + 50, int(x0) + 50] > 0.0
1109
+ assert set(driz.out_ctx.ravel().tolist()) == {0, 1}
1110
+
1111
+ # test same with numeric fillval:
1112
+ driz = resample.Drizzle(
1113
+ kernel='square',
1114
+ out_shape=out_shape,
1115
+ fillval=-1.11,
1116
+ out_img=out_img.copy(),
1117
+ out_ctx=out_ctx.copy(),
1118
+ out_wht=out_wht.copy(),
1119
+ exptime=0.0,
1120
+ )
1121
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap)
1122
+ assert np.allclose(driz.out_img[0, 0], -1.11, rtol=0.0, atol=1.0e-7)
1123
+ assert np.allclose(float(driz.fillval), -1.11, rtol=0.0, atol=np.finfo(float).eps)
1124
+
1125
+ # make sure code raises exception for unsuported fillval:
1126
+ with pytest.raises(ValueError) as err_info:
1127
+ resample.Drizzle(
1128
+ kernel='square',
1129
+ out_shape=out_shape,
1130
+ fillval="fillval",
1131
+ exptime=0.0,
1132
+ )
1133
+ assert str(err_info.value) == "could not convert string to float: 'fillval'"
1134
+
1135
+
1136
+ def test_resample_get_shape_from_pixmap():
1137
+ n = 200
1138
+ in_shape = (n, n)
1139
+
1140
+ # input coordinate grid:
1141
+ y, x = np.indices(in_shape, dtype=np.float64)
1142
+
1143
+ # simulate constant data:
1144
+ in_sci = np.ones(in_shape, dtype=np.float32)
1145
+ in_wht = np.ones(in_shape, dtype=np.float32)
1146
+
1147
+ pixmap = np.dstack([x, y])
1148
+
1149
+ driz = resample.Drizzle(
1150
+ kernel='point',
1151
+ exptime=0.0,
1152
+ )
1153
+
1154
+ driz.add_image(in_sci, weight_map=in_wht, exptime=0.1, pixmap=pixmap)
1155
+ assert driz.out_img.shape == in_shape
1156
+
1157
+
1158
+ def test_resample_counts_units():
1159
+ n = 200
1160
+ in_shape = (n, n)
1161
+
1162
+ # input coordinate grid:
1163
+ y, x = np.indices(in_shape, dtype=np.float64)
1164
+ pixmap = np.dstack([x, y])
1165
+
1166
+ # simulate constant data:
1167
+ in_sci = np.ones(in_shape, dtype=np.float32)
1168
+ in_wht = np.ones(in_shape, dtype=np.float32)
1169
+
1170
+ driz = resample.Drizzle()
1171
+ driz.add_image(in_sci, weight_map=in_wht, exptime=1.0, pixmap=pixmap, in_units='cps')
1172
+ cps_max_val = driz.out_img.max()
1173
+
1174
+ driz = resample.Drizzle()
1175
+ driz.add_image(in_sci, weight_map=in_wht, exptime=2.0, pixmap=pixmap, in_units='counts')
1176
+ counts_max_val = driz.out_img.max()
1177
+
1178
+ assert abs(counts_max_val - cps_max_val / 2.0) < 1.0e-14
1179
+
1180
+
1181
+ def test_resample_inconsistent_output():
1182
+ n = 200
1183
+ out_shape = (n, n)
1184
+
1185
+ # different shapes:
1186
+ out_img = np.zeros((n, n), dtype=np.float32)
1187
+ out_ctx = np.zeros((1, n, n + 1), dtype=np.int32)
1188
+ out_wht = np.zeros((n + 1, n + 1), dtype=np.float32)
1189
+
1190
+ # shape from out_img:
1191
+ driz = resample.Drizzle(
1192
+ kernel='point',
1193
+ exptime=0.0,
1194
+ out_img=out_img,
1195
+ )
1196
+ assert driz.out_img.shape == out_shape
1197
+
1198
+ # inconsistent shapes:
1199
+ out_shape = (n + 1, n)
1200
+ with pytest.raises(ValueError) as err_info:
1201
+ resample.Drizzle(
1202
+ kernel='point',
1203
+ exptime=0.0,
1204
+ out_shape=out_shape,
1205
+ out_img=out_img,
1206
+ out_ctx=out_ctx,
1207
+ out_wht=out_wht,
1208
+ )
1209
+ assert str(err_info.value).startswith("Inconsistent data shapes specified")
1210
+
1211
+
1212
+ def test_resample_disable_ctx():
1213
+ n = 20
1214
+ in_shape = (n, n)
1215
+
1216
+ pixmap = np.dstack(np.indices(in_shape, dtype=np.float64)[::-1])
1217
+
1218
+ # simulate constant data:
1219
+ in_sci = np.ones(in_shape, dtype=np.float32)
1220
+
1221
+ driz = resample.Drizzle(
1222
+ disable_ctx=True,
1223
+ )
1224
+
1225
+ driz.add_image(in_sci, exptime=1.0, pixmap=pixmap)
1226
+
1227
+
1228
+ @pytest.mark.parametrize(
1229
+ "fillval", ["NaN", "INDEF", "", None]
1230
+ )
1231
+ def test_nan_fillval(fillval):
1232
+ driz = resample.Drizzle(
1233
+ kernel='square',
1234
+ fillval=fillval,
1235
+ out_shape=(20, 20)
1236
+ )
1237
+
1238
+ assert np.all(np.isnan(driz.out_img))
1239
+
1240
+
1241
+ def test_resample_edge_sgarea_bug():
1242
+ """
1243
+ Test from https://github.com/spacetelescope/drizzle/issues/187
1244
+
1245
+ """
1246
+ pixmap = (np.array([
1247
+ [
1248
+ [0.31887051, 1.],
1249
+ [1.01898591, 1.],
1250
+ [1.71909665, 1.],
1251
+ ],
1252
+ [
1253
+ [0.31591881, 0.],
1254
+ [1.0160342312345672, 0.],
1255
+ [1.716145, 0.],
1256
+ ]
1257
+ ], dtype="f8"))
1258
+
1259
+ in_shape = pixmap.shape[:2]
1260
+ img = np.full(in_shape, 42, dtype=np.float32)
1261
+ out_shape = (4, 4)
1262
+
1263
+ driz = resample.Drizzle(
1264
+ kernel='square',
1265
+ fillval='nan',
1266
+ out_shape=out_shape,
1267
+ disable_ctx=True,
1268
+ )
1269
+
1270
+ driz.add_image(
1271
+ img,
1272
+ exptime=11.776,
1273
+ in_units='cps',
1274
+ pixfrac=1.0,
1275
+ pixmap=pixmap,
1276
+ scale=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
+ [0.31, 1.0],
1299
+ [1.01, 1.0],
1300
+ [2.01, 1.0],
1301
+ ],
1302
+ [
1303
+ [0.31, 0.],
1304
+ [1.01, 0.],
1305
+ [1.71, 0.],
1306
+ ]
1307
+ ], dtype="f8"))
1308
+
1309
+ in_shape = pixmap.shape[:2]
1310
+ img = np.full(in_shape, np.pi, dtype=np.float32)
1311
+ in_flux = np.sum(img)
1312
+ out_shape = (4, 4)
1313
+
1314
+ driz = resample.Drizzle(
1315
+ kernel='square',
1316
+ fillval='nan',
1317
+ out_shape=out_shape,
1318
+ disable_ctx=True,
1319
+ )
1320
+
1321
+ driz.add_image(
1322
+ img,
1323
+ exptime=11.776,
1324
+ in_units='cps',
1325
+ pixfrac=1.0,
1326
+ pixmap=pixmap,
1327
+ scale=1.0,
1328
+ wht_scale=1.0,
1329
+ )
1330
+
1331
+ out_flux = np.nansum(driz.out_img * driz.out_wht)
1332
+
1333
+ # Given this pixmap, the entire input image should fit within the output
1334
+ # image. There should be at least 7 pixels with finite values in the output
1335
+ # image. We can get more than 7 pixels with finite values due to rounding
1336
+ # errors when computing polygon intersections (those "extra" pixels should)
1337
+ # have very small weights.
1338
+ assert np.sum(driz.out_wht > 1e-30) == 7
1339
+ assert np.sum(np.isfinite(driz.out_img)) >= 7
1340
+ # output image intensity must be equal to the input image intensity:
1341
+ assert np.allclose(
1342
+ driz.out_img[np.isfinite(driz.out_img)],
1343
+ img[0, 0],
1344
+ rtol=0,
1345
+ atol=1e-6
1346
+ )
1347
+ # flux in the output image should be equal to the flux in the input image:
1348
+ assert np.allclose(out_flux, in_flux, rtol=1e-6, atol=0.0)
1349
+ # area of the signal in the input image:
1350
+ assert np.allclose(np.sum(driz.out_wht), 6.0, rtol=0, atol=1.0e-6)