1# Licensed under a 3-clause BSD style license - see LICENSE.rst
2# pylint: disable=invalid-name
3import os
4import sys
5import subprocess
6
7import pytest
8import unittest.mock as mk
9import numpy as np
10from inspect import signature
11from numpy.testing import assert_allclose, assert_equal
12
13import astropy
14from astropy.modeling.core import (Model, CompoundModel, custom_model,
15                                   SPECIAL_OPERATORS, _add_special_operator,
16                                   bind_bounding_box, bind_compound_bounding_box,
17                                   fix_inputs)
18from astropy.modeling.bounding_box import ModelBoundingBox, CompoundBoundingBox
19from astropy.modeling.separable import separability_matrix
20from astropy.modeling.parameters import Parameter
21from astropy.modeling import models
22from astropy.convolution import convolve_models
23import astropy.units as u
24from astropy.tests.helper import assert_quantity_allclose
25from astropy.utils.compat.optional_deps import HAS_SCIPY  # noqa
26import astropy.modeling.core as core
27
28
29class NonFittableModel(Model):
30    """An example class directly subclassing Model for testing."""
31
32    a = Parameter()
33
34    def __init__(self, a, model_set_axis=None):
35        super().__init__(a, model_set_axis=model_set_axis)
36
37    @staticmethod
38    def evaluate():
39        pass
40
41
42def test_Model_instance_repr_and_str():
43    m = NonFittableModel(42.5)
44    assert repr(m) == "<NonFittableModel(a=42.5)>"
45    assert (str(m) ==
46            "Model: NonFittableModel\n"
47            "Inputs: ()\n"
48            "Outputs: ()\n"
49            "Model set size: 1\n"
50            "Parameters:\n"
51            "     a  \n"
52            "    ----\n"
53            "    42.5")
54
55    assert len(m) == 1
56
57
58def test_Model_array_parameter():
59    model = models.Gaussian1D(4, 2, 1)
60    assert_allclose(model.param_sets, [[4], [2], [1]])
61
62
63def test_inputless_model():
64    """
65    Regression test for
66    https://github.com/astropy/astropy/pull/3772#issuecomment-101821641
67    """
68
69    class TestModel(Model):
70
71        n_outputs = 1
72        a = Parameter()
73
74        @staticmethod
75        def evaluate(a):
76            return a
77
78    m = TestModel(1)
79    assert m.a == 1
80    assert m() == 1
81
82    # Test array-like output
83    m = TestModel([1, 2, 3], model_set_axis=False)
84    assert len(m) == 1
85    assert np.all(m() == [1, 2, 3])
86
87    # Test a model set
88    m = TestModel(a=[1, 2, 3], model_set_axis=0)
89    assert len(m) == 3
90    assert np.all(m() == [1, 2, 3])
91
92    # Test a model set
93    m = TestModel(a=[[1, 2, 3], [4, 5, 6]], model_set_axis=0)
94    assert len(m) == 2
95    assert np.all(m() == [[1, 2, 3], [4, 5, 6]])
96
97    # Test a model set
98    m = TestModel(a=[[1, 2, 3], [4, 5, 6]], model_set_axis=np.int64(0))
99    assert len(m) == 2
100    assert np.all(m() == [[1, 2, 3], [4, 5, 6]])
101
102
103def test_ParametericModel():
104    with pytest.raises(TypeError):
105        models.Gaussian1D(1, 2, 3, wrong=4)
106
107
108def test_custom_model_signature():
109    """
110    Tests that the signatures for the __init__ and __call__
111    methods of custom models are useful.
112    """
113
114    @custom_model
115    def model_a(x):
116        return x
117
118    assert model_a.param_names == ()
119    assert model_a.n_inputs == 1
120    sig = signature(model_a.__init__)
121    assert list(sig.parameters.keys()) == ['self', 'args', 'meta', 'name', 'kwargs']
122    sig = signature(model_a.__call__)
123    assert list(sig.parameters.keys()) == ['self', 'inputs', 'model_set_axis',
124                                           'with_bounding_box', 'fill_value',
125                                           'equivalencies', 'inputs_map', 'new_inputs']
126
127    @custom_model
128    def model_b(x, a=1, b=2):
129        return x + a + b
130
131    assert model_b.param_names == ('a', 'b')
132    assert model_b.n_inputs == 1
133    sig = signature(model_b.__init__)
134    assert list(sig.parameters.keys()) == ['self', 'a', 'b', 'kwargs']
135    assert [x.default for x in sig.parameters.values()] == [sig.empty, 1, 2, sig.empty]
136    sig = signature(model_b.__call__)
137    assert list(sig.parameters.keys()) == ['self', 'inputs', 'model_set_axis',
138                                           'with_bounding_box', 'fill_value',
139                                           'equivalencies', 'inputs_map', 'new_inputs']
140
141    @custom_model
142    def model_c(x, y, a=1, b=2):
143        return x + y + a + b
144
145    assert model_c.param_names == ('a', 'b')
146    assert model_c.n_inputs == 2
147    sig = signature(model_c.__init__)
148    assert list(sig.parameters.keys()) == ['self', 'a', 'b', 'kwargs']
149    assert [x.default for x in sig.parameters.values()] == [sig.empty, 1, 2, sig.empty]
150    sig = signature(model_c.__call__)
151    assert list(sig.parameters.keys()) == ['self', 'inputs', 'model_set_axis',
152                                           'with_bounding_box', 'fill_value',
153                                           'equivalencies', 'inputs_map', 'new_inputs']
154
155
156def test_custom_model_subclass():
157    """Test that custom models can be subclassed."""
158
159    @custom_model
160    def model_a(x, a=1):
161        return x * a
162
163    class model_b(model_a):
164        # Override the evaluate from model_a
165        @classmethod
166        def evaluate(cls, x, a):
167            return -super().evaluate(x, a)
168
169    b = model_b()
170    assert b.param_names == ('a',)
171    assert b.a == 1
172    assert b(1) == -1
173
174    sig = signature(model_b.__init__)
175    assert list(sig.parameters.keys()) == ['self', 'a', 'kwargs']
176    sig = signature(model_b.__call__)
177    assert list(sig.parameters.keys()) == ['self', 'inputs', 'model_set_axis',
178                                           'with_bounding_box', 'fill_value',
179                                           'equivalencies', 'inputs_map', 'new_inputs']
180
181
182def test_custom_model_parametrized_decorator():
183    """Tests using custom_model as a decorator with parameters."""
184
185    def cosine(x, amplitude=1):
186        return [amplitude * np.cos(x)]
187
188    @custom_model(fit_deriv=cosine)
189    def sine(x, amplitude=1):
190        return amplitude * np.sin(x)
191
192    assert issubclass(sine, Model)
193    s = sine(2)
194    assert_allclose(s(np.pi / 2), 2)
195    assert_allclose(s.fit_deriv(0, 2), 2)
196
197
198def test_custom_model_n_outputs():
199    """
200    Test creating a custom_model which has more than one output, which
201    requires special handling.
202        Demonstrates issue #11791's ``n_outputs`` error has been solved
203    """
204
205    @custom_model
206    def model(x, y, n_outputs=2):
207        return x+1, y+1
208
209    m = model()
210    assert not isinstance(m.n_outputs, Parameter)
211    assert isinstance(m.n_outputs, int)
212    assert m.n_outputs == 2
213    assert m.outputs == ('x0', 'x1')
214    assert (separability_matrix(m) == [[True, True],
215                                       [True, True]]).all()
216
217    @custom_model
218    def model(x, y, z, n_outputs=3):
219        return x+1, y+1, z+1
220
221    m = model()
222    assert not isinstance(m.n_outputs, Parameter)
223    assert isinstance(m.n_outputs, int)
224    assert m.n_outputs == 3
225    assert m.outputs == ('x0', 'x1', 'x2')
226    assert (separability_matrix(m) == [[True, True, True],
227                                       [True, True, True],
228                                       [True, True, True]]).all()
229
230
231def test_custom_model_settable_parameters():
232    """
233    Test creating a custom_model which specifically sets adjustable model
234    parameters.
235        Demonstrates part of issue #11791's notes about what passed parameters
236        should/shouldn't be allowed. In this case, settable parameters
237        should be allowed to have defaults set.
238    """
239    @custom_model
240    def model(x, y, n_outputs=2, bounding_box=((1, 2), (3, 4))):
241        return x+1, y+1
242
243    m = model()
244    assert m.n_outputs == 2
245    assert m.bounding_box == ((1, 2), (3, 4))
246    m.bounding_box = ((9, 10), (11, 12))
247    assert m.bounding_box == ((9, 10), (11, 12))
248    m = model(bounding_box=((5, 6), (7, 8)))
249    assert m.n_outputs == 2
250    assert m.bounding_box == ((5, 6), (7, 8))
251    m.bounding_box = ((9, 10), (11, 12))
252    assert m.bounding_box == ((9, 10), (11, 12))
253
254    @custom_model
255    def model(x, y, n_outputs=2, outputs=('z0', 'z1')):
256        return x+1, y+1
257
258    m = model()
259    assert m.n_outputs == 2
260    assert m.outputs == ('z0', 'z1')
261    m.outputs = ('a0', 'a1')
262    assert m.outputs == ('a0', 'a1')
263    m = model(outputs=('w0', 'w1'))
264    assert m.n_outputs == 2
265    assert m.outputs == ('w0', 'w1')
266    m.outputs = ('a0', 'a1')
267    assert m.outputs == ('a0', 'a1')
268
269
270def test_custom_model_regected_parameters():
271    """
272    Test creating a custom_model which attempts to override non-overridable
273    parameters.
274        Demonstrates part of issue #11791's notes about what passed parameters
275        should/shouldn't be allowed. In this case, non-settable parameters
276        should raise an error (unexpected behavior may occur).
277    """
278
279    with pytest.raises(ValueError,
280                       match=r"Parameter 'n_inputs' cannot be a model property: *"):
281        @custom_model
282        def model(x, y, n_outputs=2, n_inputs=3):
283            return x+1, y+1
284
285    with pytest.raises(ValueError,
286                       match=r"Parameter 'uses_quantity' cannot be a model property: *"):
287        @custom_model
288        def model(x, y, n_outputs=2, uses_quantity=True):
289            return x+1, y+1
290
291
292def test_custom_inverse():
293    """Test setting a custom inverse on a model."""
294
295    p = models.Polynomial1D(1, c0=-2, c1=3)
296    # A trivial inverse for a trivial polynomial
297    inv = models.Polynomial1D(1, c0=(2./3.), c1=(1./3.))
298
299    with pytest.raises(NotImplementedError):
300        p.inverse
301
302    p.inverse = inv
303
304    x = np.arange(100)
305
306    assert_allclose(x, p(p.inverse(x)))
307    assert_allclose(x, p.inverse(p(x)))
308
309    p.inverse = None
310
311    with pytest.raises(NotImplementedError):
312        p.inverse
313
314
315def test_custom_inverse_reset():
316    """Test resetting a custom inverse to the model's default inverse."""
317
318    class TestModel(Model):
319        n_inputs = 0
320        outputs = ('y',)
321
322        @property
323        def inverse(self):
324            return models.Shift()
325
326        @staticmethod
327        def evaluate():
328            return 0
329
330    # The above test model has no meaning, nor does its inverse--this just
331    # tests that setting an inverse and resetting to the default inverse works
332
333    m = TestModel()
334    assert isinstance(m.inverse, models.Shift)
335
336    m.inverse = models.Scale()
337    assert isinstance(m.inverse, models.Scale)
338
339    del m.inverse
340    assert isinstance(m.inverse, models.Shift)
341
342
343def test_render_model_2d():
344    imshape = (71, 141)
345    image = np.zeros(imshape)
346    coords = y, x = np.indices(imshape)
347
348    model = models.Gaussian2D(x_stddev=6.1, y_stddev=3.9, theta=np.pi / 3)
349
350    # test points for edges
351    ye, xe = [0, 35, 70], [0, 70, 140]
352    # test points for floating point positions
353    yf, xf = [35.1, 35.5, 35.9], [70.1, 70.5, 70.9]
354
355    test_pts = [(a, b) for a in xe for b in ye]
356    test_pts += [(a, b) for a in xf for b in yf]
357
358    for x0, y0 in test_pts:
359        model.x_mean = x0
360        model.y_mean = y0
361        expected = model(x, y)
362        for xy in [coords, None]:
363            for im in [image.copy(), None]:
364                if (im is None) & (xy is None):
365                    # this case is tested in Fittable2DModelTester
366                    continue
367                actual = model.render(out=im, coords=xy)
368                if im is None:
369                    assert_allclose(actual, model.render(coords=xy))
370                # assert images match
371                assert_allclose(expected, actual, atol=3e-7)
372                # assert model fully captured
373                if (x0, y0) == (70, 35):
374                    boxed = model.render()
375                    flux = np.sum(expected)
376                    assert ((flux - np.sum(boxed)) / flux) < 1e-7
377    # test an error is raised when the bounding box is larger than the input array
378    try:
379        actual = model.render(out=np.zeros((1, 1)))
380    except ValueError:
381        pass
382
383
384def test_render_model_1d():
385    npix = 101
386    image = np.zeros(npix)
387    coords = np.arange(npix)
388
389    model = models.Gaussian1D()
390
391    # test points
392    test_pts = [0, 49.1, 49.5, 49.9, 100]
393
394    # test widths
395    test_stdv = np.arange(5.5, 6.7, .2)
396
397    for x0, stdv in [(p, s) for p in test_pts for s in test_stdv]:
398        model.mean = x0
399        model.stddev = stdv
400        expected = model(coords)
401        for x in [coords, None]:
402            for im in [image.copy(), None]:
403                if (im is None) & (x is None):
404                    # this case is tested in Fittable1DModelTester
405                    continue
406                actual = model.render(out=im, coords=x)
407                # assert images match
408                assert_allclose(expected, actual, atol=3e-7)
409                # assert model fully captured
410                if (x0, stdv) == (49.5, 5.5):
411                    boxed = model.render()
412                    flux = np.sum(expected)
413                    assert ((flux - np.sum(boxed)) / flux) < 1e-7
414
415
416@pytest.mark.filterwarnings('ignore:invalid value encountered in less')
417def test_render_model_3d():
418    imshape = (17, 21, 27)
419    image = np.zeros(imshape)
420    coords = np.indices(imshape)
421
422    def ellipsoid(x, y, z, x0=13., y0=10., z0=8., a=4., b=3., c=2., amp=1.):
423        rsq = ((x - x0) / a) ** 2 + ((y - y0) / b) ** 2 + ((z - z0) / c) ** 2
424        val = (rsq < 1) * amp
425        return val
426
427    class Ellipsoid3D(custom_model(ellipsoid)):
428        @property
429        def bounding_box(self):
430            return ((self.z0 - self.c, self.z0 + self.c),
431                    (self.y0 - self.b, self.y0 + self.b),
432                    (self.x0 - self.a, self.x0 + self.a))
433
434    model = Ellipsoid3D()
435
436    # test points for edges
437    ze, ye, xe = [0, 8, 16], [0, 10, 20], [0, 13, 26]
438    # test points for floating point positions
439    zf, yf, xf = [8.1, 8.5, 8.9], [10.1, 10.5, 10.9], [13.1, 13.5, 13.9]
440
441    test_pts = [(x, y, z) for x in xe for y in ye for z in ze]
442    test_pts += [(x, y, z) for x in xf for y in yf for z in zf]
443
444    for x0, y0, z0 in test_pts:
445        model.x0 = x0
446        model.y0 = y0
447        model.z0 = z0
448        expected = model(*coords[::-1])
449        for c in [coords, None]:
450            for im in [image.copy(), None]:
451                if (im is None) & (c is None):
452                    continue
453                actual = model.render(out=im, coords=c)
454                boxed = model.render()
455                # assert images match
456                assert_allclose(expected, actual)
457                # assert model fully captured
458                if (z0, y0, x0) == (8, 10, 13):
459                    boxed = model.render()
460                    assert (np.sum(expected) - np.sum(boxed)) == 0
461
462
463def test_render_model_out_dtype():
464    """Test different out.dtype for model.render."""
465    for model in [models.Gaussian2D(), models.Gaussian2D() + models.Planar2D()]:
466        for dtype in [np.float64, np.float32, np.complex64]:
467            im = np.zeros((40, 40), dtype=dtype)
468            imout = model.render(out=im)
469            assert imout is im
470            assert imout.sum() != 0
471        with pytest.raises(TypeError):
472            im = np.zeros((40, 40), dtype=np.int32)
473            imout = model.render(out=im)
474
475
476def test_custom_bounding_box_1d():
477    """
478    Tests that the bounding_box setter works.
479    """
480    # 1D models
481    g1 = models.Gaussian1D()
482    bb = g1.bounding_box
483    expected = g1.render()
484
485    # assign the same bounding_box, now through the bounding_box setter
486    g1.bounding_box = bb
487    assert_allclose(g1.render(), expected)
488
489    # 2D models
490    g2 = models.Gaussian2D()
491    bb = g2.bounding_box
492    expected = g2.render()
493
494    # assign the same bounding_box, now through the bounding_box setter
495    g2.bounding_box = bb
496    assert_allclose(g2.render(), expected)
497
498
499def test_n_submodels_in_single_models():
500    assert models.Gaussian1D().n_submodels == 1
501    assert models.Gaussian2D().n_submodels == 1
502
503
504def test_compound_deepcopy():
505    model = (models.Gaussian1D(10, 2, 3) | models.Shift(2)) & models.Rotation2D(21.3)
506    new_model = model.deepcopy()
507    assert id(model) != id(new_model)
508    assert id(model._leaflist) != id(new_model._leaflist)
509    assert id(model[0]) != id(new_model[0])
510    assert id(model[1]) != id(new_model[1])
511    assert id(model[2]) != id(new_model[2])
512
513
514@pytest.mark.skipif('not HAS_SCIPY')
515def test_units_with_bounding_box():
516    points = np.arange(10, 20)
517    table = np.arange(10) * u.Angstrom
518    t = models.Tabular1D(points, lookup_table=table)
519
520    assert isinstance(t(10), u.Quantity)
521    assert isinstance(t(10, with_bounding_box=True), u.Quantity)
522
523    assert_quantity_allclose(t(10), t(10, with_bounding_box=True))
524
525
526RENAMED_MODEL = models.Gaussian1D.rename('CustomGaussian')
527
528MODEL_RENAME_CODE = """
529from astropy.modeling.models import Gaussian1D
530print(repr(Gaussian1D))
531print(repr(Gaussian1D.rename('CustomGaussian')))
532""".strip()
533
534MODEL_RENAME_EXPECTED = b"""
535<class 'astropy.modeling.functional_models.Gaussian1D'>
536Name: Gaussian1D
537N_inputs: 1
538N_outputs: 1
539Fittable parameters: ('amplitude', 'mean', 'stddev')
540<class '__main__.CustomGaussian'>
541Name: CustomGaussian (Gaussian1D)
542N_inputs: 1
543N_outputs: 1
544Fittable parameters: ('amplitude', 'mean', 'stddev')
545""".strip()
546
547
548def test_rename_path(tmpdir):
549
550    # Regression test for a bug that caused the path to the class to be
551    # incorrect in a renamed model's __repr__.
552
553    assert repr(RENAMED_MODEL).splitlines()[0] == "<class 'astropy.modeling.tests.test_core.CustomGaussian'>"
554
555    # Make sure that when called from a user script, the class name includes
556    # __main__.
557
558    env = os.environ.copy()
559    paths = [os.path.dirname(astropy.__path__[0])] + sys.path
560    env['PYTHONPATH'] = os.pathsep.join(paths)
561
562    script = tmpdir.join('rename.py').strpath
563    with open(script, 'w') as f:
564        f.write(MODEL_RENAME_CODE)
565
566    output = subprocess.check_output([sys.executable, script], env=env)
567    assert output.splitlines() == MODEL_RENAME_EXPECTED.splitlines()
568
569
570@pytest.mark.parametrize('model_class',
571                         [models.Gaussian1D, models.Polynomial1D,
572                          models.Shift, models.Tabular1D])
573def test_rename_1d(model_class):
574    new_model = model_class.rename(name='Test1D')
575    assert new_model.name == 'Test1D'
576
577
578@pytest.mark.parametrize('model_class',
579                         [models.Gaussian2D, models.Polynomial2D, models.Tabular2D])
580def test_rename_2d(model_class):
581    new_model = model_class.rename(name='Test2D')
582    assert new_model.name == 'Test2D'
583
584
585def test_fix_inputs_integer():
586    """
587    Tests that numpy integers can be passed as dictionary keys to fix_inputs
588    Issue #11358
589    """
590    m = models.Identity(2)
591
592    mf = models.fix_inputs(m, {1: 22})
593    assert mf(1) == (1, 22)
594
595    mf_int32 = models.fix_inputs(m, {np.int32(1): 33})
596    assert mf_int32(1) == (1, 33)
597
598    mf_int64 = models.fix_inputs(m, {np.int64(1): 44})
599    assert mf_int64(1) == (1, 44)
600
601
602def test_fix_inputs_empty_dict():
603    """
604    Tests that empty dictionary can be passed to fix_inputs
605    Issue #11355
606    """
607    m = models.Identity(2)
608
609    mf = models.fix_inputs(m, {})
610    assert mf(1, 2) == (1, 2)
611
612
613def test_rename_inputs_outputs():
614    g2 = models.Gaussian2D(10, 2, 3, 1, 2)
615    assert g2.inputs == ("x", "y")
616    assert g2.outputs == ("z",)
617
618    with pytest.raises(ValueError):
619        g2.inputs = ("w", )
620
621    with pytest.raises(ValueError):
622        g2.outputs = ("w", "e")
623
624
625def test__prepare_output_single_model():
626    model = models.Gaussian1D()
627
628    # No broadcast
629    assert (np.array([1, 2]) ==
630            model._prepare_output_single_model(np.array([1, 2]), None)).all()
631
632    # Broadcast to scalar
633    assert 1 == model._prepare_output_single_model(np.array([1]), ())
634    assert 2 == model._prepare_output_single_model(np.asanyarray(2), ())
635
636    # Broadcast reshape
637    output = np.array([[1, 2, 3],
638                       [4, 5, 6]])
639    reshape = np.array([[1, 2],
640                        [3, 4],
641                        [5, 6]])
642    assert (output == model._prepare_output_single_model(output, (2, 3))).all()
643    assert (reshape == model._prepare_output_single_model(output, (3, 2))).all()
644
645    # Broadcast reshape scalar
646    assert 1 == model._prepare_output_single_model(np.array([1]), (1, 2))
647    assert 2 == model._prepare_output_single_model(np.asanyarray(2), (3, 4))
648
649    # Fail to broadcast
650    assert (output == model._prepare_output_single_model(output, (1, 2))).all()
651    assert (output == model._prepare_output_single_model(output, (3, 4))).all()
652
653
654def test_prepare_outputs_mixed_broadcast():
655    """
656    Tests that _prepare_outputs_single_model does not fail when a smaller
657    array is passed as first input, but output is broadcast to larger
658    array.
659    Issue #10170
660    """
661
662    model = models.Gaussian2D(1, 2, 3, 4, 5)
663
664    output = model([1, 2], 3)
665    assert output.shape == (2,)
666    np.testing.assert_array_equal(output, [0.9692332344763441, 1.0])
667
668    output = model(4, [5, 6])
669    assert output.shape == (2,)
670    np.testing.assert_array_equal(output, [0.8146473164114145, 0.7371233743916278])
671
672
673def test_prepare_outputs_complex_reshape():
674    x = np.array([[1,  2,  3,  4,  5],
675                  [6,  7,  8,  9,  10],
676                  [11, 12, 13, 14, 15]])
677    y = np.array([[16, 17, 18, 19, 20],
678                  [21, 22, 23, 24, 25],
679                  [26, 27, 28, 29, 30]])
680
681    m = models.Identity(3) | models.Mapping((2, 1, 0))
682    m.bounding_box = ((0, 100), (0, 200), (0, 50))
683    mf = models.fix_inputs(m, {2: 22})
684    t = mf | models.Mapping((2, 1), n_inputs=3)
685
686    output = mf(1, 2)
687    assert output == (22, 2, 1)
688
689    output = t(1, 2)
690    assert output == (1, 2)
691
692    output = t(x, y)
693    assert len(output) == 2
694    np.testing.assert_array_equal(output[0], x)
695    np.testing.assert_array_equal(output[1], y)
696
697    m = models.Identity(3) | models.Mapping((0, 1, 2))
698    m.bounding_box = ((0, 100), (0, 200), (0, 50))
699    mf = models.fix_inputs(m, {2: 22})
700    t = mf | models.Mapping((0, 1), n_inputs=3)
701
702    output = mf(1, 2)
703    assert output == (1, 2, 22)
704
705    output = t(1, 2)
706    assert output == (1, 2)
707
708    output = t(x, y)
709    assert len(output) == 2
710    np.testing.assert_array_equal(output[0], x)
711    np.testing.assert_array_equal(output[1], y)
712
713
714def test_prepare_outputs_single_entry_vector():
715    """
716    jwst and gwcs both require that single entry vectors produce single entry output vectors, not scalars. This
717    tests for that behavior.
718    """
719
720    model = models.Gaussian2D(1, 2, 3, 4, 5)
721
722    output = model(np.array([1]), np.array([2]))
723    assert output.shape == (1,)
724    np.testing.assert_array_equal(output, [0.9500411305585278])
725
726
727@pytest.mark.skipif('not HAS_SCIPY')
728@pytest.mark.filterwarnings('ignore: Using a non-tuple')
729def test_prepare_outputs_sparse_grid():
730    """
731    Test to show that #11060 has been solved.
732    """
733
734    shape = (3, 3)
735    data = np.arange(np.product(shape)).reshape(shape) * u.m / u.s
736
737    points_unit = u.pix
738    points = [np.arange(size) * points_unit for size in shape]
739
740    kwargs = {
741        'bounds_error': False,
742        'fill_value': np.nan,
743        'method': 'nearest',
744    }
745
746    transform = models.Tabular2D(points, data, **kwargs)
747    truth = np.array([[0., 1., 2.],
748                      [3., 4., 5.],
749                      [6., 7., 8.]]) * u.m / u.s
750
751    points = np.meshgrid(np.arange(3), np.arange(3), indexing='ij', sparse=True)
752    x = points[0] * u.pix
753    y = points[1] * u.pix
754    value = transform(x, y)
755    assert (value == truth).all()
756
757    points = np.meshgrid(np.arange(3), np.arange(3), indexing='ij', sparse=False) * u.pix
758    value = transform(*points)
759    assert (value == truth).all()
760
761
762def test_coerce_units():
763    model = models.Polynomial1D(1, c0=1, c1=2)
764
765    with pytest.raises(u.UnitsError):
766        model(u.Quantity(10, u.m))
767
768    with_input_units = model.coerce_units({"x": u.m})
769    result = with_input_units(u.Quantity(10, u.m))
770    assert np.isclose(result, 21.0)
771
772    with_input_units_tuple = model.coerce_units((u.m,))
773    result = with_input_units_tuple(u.Quantity(10, u.m))
774    assert np.isclose(result, 21.0)
775
776    with_return_units = model.coerce_units(return_units={"y": u.s})
777    result = with_return_units(10)
778    assert np.isclose(result.value, 21.0)
779    assert result.unit == u.s
780
781    with_return_units_tuple = model.coerce_units(return_units=(u.s,))
782    result = with_return_units_tuple(10)
783    assert np.isclose(result.value, 21.0)
784    assert result.unit == u.s
785
786    with_both = model.coerce_units({"x": u.m}, {"y": u.s})
787
788    result = with_both(u.Quantity(10, u.m))
789    assert np.isclose(result.value, 21.0)
790    assert result.unit == u.s
791
792    with pytest.raises(ValueError, match=r"input_units keys.*do not match model inputs"):
793        model.coerce_units({"q": u.m})
794
795    with pytest.raises(ValueError, match=r"input_units length does not match n_inputs"):
796        model.coerce_units((u.m, u.s))
797
798    model_with_existing_input_units = models.BlackBody()
799    with pytest.raises(ValueError, match=r"Cannot specify input_units for model with existing input units"):
800        model_with_existing_input_units.coerce_units({"x": u.m})
801
802    with pytest.raises(ValueError, match=r"return_units keys.*do not match model outputs"):
803        model.coerce_units(return_units={"q": u.m})
804
805    with pytest.raises(ValueError, match=r"return_units length does not match n_outputs"):
806        model.coerce_units(return_units=(u.m, u.s))
807
808
809def test_bounding_box_general_inverse():
810    model = NonFittableModel(42.5)
811
812    with pytest.raises(NotImplementedError):
813        model.bounding_box
814    model.bounding_box = ()
815    assert model.bounding_box.bounding_box() == ()
816
817    model.inverse = NonFittableModel(3.14)
818    inverse_model = model.inverse
819    with pytest.raises(NotImplementedError):
820        inverse_model.bounding_box
821
822
823def test__add_special_operator():
824    sop_name = 'name'
825    sop = 'value'
826
827    key = _add_special_operator(sop_name, 'value')
828    assert key[0] == sop_name
829    assert key[1] == SPECIAL_OPERATORS._unique_id
830
831    assert key in SPECIAL_OPERATORS
832    assert SPECIAL_OPERATORS[key] == sop
833
834
835def test_print_special_operator_CompoundModel(capsys):
836    """
837    Test that issue #11310 has been fixed
838    """
839
840    model = convolve_models(models.Sersic2D(), models.Gaussian2D())
841    print(model)
842
843    true_out = "Model: CompoundModel\n" +\
844               "Inputs: ('x', 'y')\n" +\
845               "Outputs: ('z',)\n" +\
846               "Model set size: 1\n" +\
847               "Expression: convolve_fft (([0]), ([1]))\n" +\
848               "Components: \n" +\
849               "    [0]: <Sersic2D(amplitude=1., r_eff=1., n=4., x_0=0., y_0=0., ellip=0., theta=0.)>\n" +\
850               "\n" +\
851               "    [1]: <Gaussian2D(amplitude=1., x_mean=0., y_mean=0., x_stddev=1., y_stddev=1., theta=0.)>\n" +\
852               "Parameters:\n" +\
853               "    amplitude_0 r_eff_0 n_0 x_0_0 y_0_0 ... y_mean_1 x_stddev_1 y_stddev_1 theta_1\n" +\
854               "    ----------- ------- --- ----- ----- ... -------- ---------- ---------- -------\n" +\
855               "            1.0     1.0 4.0   0.0   0.0 ...      0.0        1.0        1.0     0.0\n"
856
857    out, err = capsys.readouterr()
858    assert err == ''
859    assert out == true_out
860
861
862def test__validate_input_shape():
863    model = models.Gaussian1D()
864    model._n_models = 2
865
866    _input = np.array([[1, 2, 3],
867                       [4, 5, 6]])
868
869    # Successful validation
870    assert model._validate_input_shape(_input, 0, model.inputs, 1, False) == (2, 3)
871
872    # Fail number of axes
873    with pytest.raises(ValueError) as err:
874        model._validate_input_shape(_input, 0, model.inputs, 2, True)
875    assert str(err.value) == \
876        "For model_set_axis=2, all inputs must be at least 3-dimensional."
877
878    # Fail number of models (has argname)
879    with pytest.raises(ValueError) as err:
880        model._validate_input_shape(_input, 0, model.inputs, 1, True)
881    assert str(err.value) == \
882        "Input argument 'x' does not have the correct dimensions in model_set_axis=1 " +\
883        "for a model set with n_models=2."
884
885    # Fail number of models  (no argname)
886    with pytest.raises(ValueError) as err:
887        model._validate_input_shape(_input, 0, [], 1, True)
888    assert str(err.value) == \
889        "Input argument '0' does not have the correct dimensions in model_set_axis=1 " +\
890        "for a model set with n_models=2."
891
892
893def test__validate_input_shapes():
894    model = models.Gaussian1D()
895    model._n_models = 2
896    inputs = [mk.MagicMock() for _ in range(3)]
897    argnames = mk.MagicMock()
898    model_set_axis = mk.MagicMock()
899    all_shapes = [mk.MagicMock() for _ in inputs]
900
901    # Successful validation
902    with mk.patch.object(Model, '_validate_input_shape',
903                         autospec=True, side_effect=all_shapes) as mkValidate:
904        with mk.patch.object(core, 'check_broadcast',
905                             autospec=True) as mkCheck:
906            assert mkCheck.return_value == \
907                model._validate_input_shapes(inputs, argnames, model_set_axis)
908            assert mkCheck.call_args_list == [mk.call(*all_shapes)]
909            assert mkValidate.call_args_list == \
910                [mk.call(model, _input, idx, argnames, model_set_axis, True)
911                 for idx, _input in enumerate(inputs)]
912
913    # Fail check_broadcast
914    with mk.patch.object(Model, '_validate_input_shape',
915                         autospec=True, side_effect=all_shapes) as mkValidate:
916        with mk.patch.object(core, 'check_broadcast',
917                             autospec=True, return_value=None) as mkCheck:
918            with pytest.raises(ValueError) as err:
919                model._validate_input_shapes(inputs, argnames, model_set_axis)
920            assert str(err.value) == \
921                "All inputs must have identical shapes or must be scalars."
922            assert mkCheck.call_args_list == [mk.call(*all_shapes)]
923            assert mkValidate.call_args_list == \
924                [mk.call(model, _input, idx, argnames, model_set_axis, True)
925                 for idx, _input in enumerate(inputs)]
926
927
928def test__remove_axes_from_shape():
929    model = models.Gaussian1D()
930
931    # len(shape) == 0
932    assert model._remove_axes_from_shape((), mk.MagicMock()) == ()
933
934    # axis < 0
935    assert model._remove_axes_from_shape((1, 2, 3), -1) == (1, 2)
936    assert model._remove_axes_from_shape((1, 2, 3), -2) == (1, 3)
937    assert model._remove_axes_from_shape((1, 2, 3), -3) == (2, 3)
938
939    # axis >= len(shape)
940    assert model._remove_axes_from_shape((1, 2, 3), 3) == ()
941    assert model._remove_axes_from_shape((1, 2, 3), 4) == ()
942
943    # 0 <= axis < len(shape)
944    assert model._remove_axes_from_shape((1, 2, 3), 0) == (2, 3)
945    assert model._remove_axes_from_shape((1, 2, 3), 1) == (3,)
946    assert model._remove_axes_from_shape((1, 2, 3), 2) == ()
947
948
949def test_get_bounding_box():
950    model = models.Const2D(2)
951
952    # No with_bbox
953    assert model.get_bounding_box(False) is None
954
955    # No bounding_box
956    with pytest.raises(NotImplementedError):
957        model.bounding_box
958    assert model.get_bounding_box(True) is None
959
960    # Normal bounding_box
961    model.bounding_box = ((0, 1), (0, 1))
962    assert not isinstance(model.bounding_box, CompoundBoundingBox)
963    assert model.get_bounding_box(True) == ((0, 1), (0, 1))
964
965    # CompoundBoundingBox with no removal
966    bbox = CompoundBoundingBox.validate(model, {(1,): ((-1, 0), (-1, 0)), (2,): ((0, 1), (0, 1))},
967                                        selector_args=[('y', False)])
968    model.bounding_box = bbox
969    assert isinstance(model.bounding_box, CompoundBoundingBox)
970    # Get using argument not with_bbox
971    assert model.get_bounding_box(True) == bbox
972    # Get using with_bbox not argument
973    assert model.get_bounding_box((1,)) == ((-1, 0), (-1, 0))
974    assert model.get_bounding_box((2,)) == ((0, 1), (0, 1))
975
976
977def test_compound_bounding_box():
978    model = models.Gaussian1D()
979    truth = models.Gaussian1D()
980    bbox1 = CompoundBoundingBox.validate(model, {(1,): (-1, 0), (2,): (0, 1)},
981                                         selector_args=[('x', False)])
982    bbox2 = CompoundBoundingBox.validate(model, {(-0.5,): (-1, 0), (0.5,): (0, 1)},
983                                         selector_args=[('x', False)])
984
985    # Using with_bounding_box to pass a selector
986    model.bounding_box = bbox1
987    assert model(-0.5) == truth(-0.5)
988    assert model(-0.5, with_bounding_box=(1,)) == truth(-0.5)
989    assert np.isnan(model(-0.5, with_bounding_box=(2,)))
990    assert model(0.5) == truth(0.5)
991    assert model(0.5, with_bounding_box=(2,)) == truth(0.5)
992    assert np.isnan(model(0.5, with_bounding_box=(1,)))
993
994    # Using argument value to pass bounding_box
995    model.bounding_box = bbox2
996    assert model(-0.5) == truth(-0.5)
997    assert model(-0.5, with_bounding_box=True) == truth(-0.5)
998    assert model(0.5) == truth(0.5)
999    assert model(0.5, with_bounding_box=True) == truth(0.5)
1000    with pytest.raises(RuntimeError):
1001        model(0, with_bounding_box=True)
1002
1003    model1 = models.Gaussian1D()
1004    truth1 = models.Gaussian1D()
1005    model2 = models.Const1D(2)
1006    truth2 = models.Const1D(2)
1007    model = model1 + model2
1008    truth = truth1 + truth2
1009    assert isinstance(model, CompoundModel)
1010
1011    model.bounding_box = bbox1
1012    assert model(-0.5) == truth(-0.5)
1013    assert model(-0.5, with_bounding_box=1) == truth(-0.5)
1014    assert np.isnan(model(-0.5, with_bounding_box=(2,)))
1015    assert model(0.5) == truth(0.5)
1016    assert model(0.5, with_bounding_box=2) == truth(0.5)
1017    assert np.isnan(model(0.5, with_bounding_box=(1,)))
1018
1019    model.bounding_box = bbox2
1020    assert model(-0.5) == truth(-0.5)
1021    assert model(-0.5, with_bounding_box=True) == truth(-0.5)
1022    assert model(0.5) == truth(0.5)
1023    assert model(0.5, with_bounding_box=True) == truth(0.5)
1024    with pytest.raises(RuntimeError):
1025        model(0, with_bounding_box=True)
1026
1027
1028def test_bind_bounding_box():
1029    model = models.Polynomial2D(3)
1030    bbox = ((-1, 1), (-2, 2))
1031
1032    bind_bounding_box(model, bbox)
1033    assert model.get_bounding_box() is not None
1034    assert model.bounding_box == bbox
1035    assert model.bounding_box['x'] == (-2, 2)
1036    assert model.bounding_box['y'] == (-1, 1)
1037
1038    bind_bounding_box(model, bbox, order='F')
1039    assert model.get_bounding_box() is not None
1040    assert model.bounding_box == bbox
1041    assert model.bounding_box['x'] == (-1, 1)
1042    assert model.bounding_box['y'] == (-2, 2)
1043
1044
1045def test_bind_compound_bounding_box_using_with_bounding_box_select():
1046    """
1047    This demonstrates how to bind multiple bounding_boxes which are
1048    selectable using the `with_bounding_box`, note there must be a
1049    fall-back to implicit.
1050    """
1051    model = models.Gaussian1D()
1052    truth = models.Gaussian1D()
1053
1054    bbox = (0, 1)
1055    with pytest.raises(AttributeError):
1056        bind_compound_bounding_box(model, bbox, 'x')
1057
1058    bbox = {0: (-1, 0), 1: (0, 1)}
1059    bind_compound_bounding_box(model, bbox, [('x', False)])
1060
1061    # No bounding box
1062    assert model(-0.5) == truth(-0.5)
1063    assert model(0.5) == truth(0.5)
1064    assert model(0) == truth(0)
1065    assert model(1) == truth(1)
1066
1067    # `with_bounding_box` selects as `-0.5` will not be a key
1068    assert model(-0.5, with_bounding_box=0) == truth(-0.5)
1069    assert np.isnan(model(-0.5, with_bounding_box=1))
1070
1071    # `with_bounding_box` selects as `0.5` will not be a key
1072    assert model(0.5, with_bounding_box=1) == truth(0.5)
1073    assert np.isnan(model(0.5, with_bounding_box=(0,)))
1074
1075    # Fall back onto implicit selector
1076    assert model(0, with_bounding_box=True) == truth(0)
1077    assert model(1, with_bounding_box=True) == truth(1)
1078
1079    # Attempt to fall-back on implicit selector, but no bounding_box
1080    with pytest.raises(RuntimeError):
1081        model(0.5, with_bounding_box=True)
1082
1083    # Override implicit selector
1084    assert np.isnan(model(1, with_bounding_box=0))
1085
1086
1087def test_fix_inputs_compound_bounding_box():
1088    base_model = models.Gaussian2D(1, 2, 3, 4, 5)
1089    bbox = {2.5: (-1, 1), 3.14: (-7, 3)}
1090
1091    model = fix_inputs(base_model, {'y': 2.5}, bounding_boxes=bbox)
1092    assert model.bounding_box == (-1, 1)
1093    model = fix_inputs(base_model, {'x': 2.5}, bounding_boxes=bbox)
1094    assert model.bounding_box == (-1, 1)
1095
1096    model = fix_inputs(base_model, {'y': 2.5}, bounding_boxes=bbox, selector_args=(('y', True),))
1097    assert model.bounding_box == (-1, 1)
1098    model = fix_inputs(base_model, {'x': 2.5}, bounding_boxes=bbox, selector_args=(('x', True),))
1099    assert model.bounding_box == (-1, 1)
1100    model = fix_inputs(base_model, {'x': 2.5}, bounding_boxes=bbox, selector_args=((0, True),))
1101    assert model.bounding_box == (-1, 1)
1102
1103    base_model = models.Identity(4)
1104    bbox = {(2.5, 1.3): ((-1, 1), (-3, 3)), (2.5, 2.71): ((-3, 3), (-1, 1))}
1105
1106    model = fix_inputs(base_model, {'x0': 2.5, 'x1': 1.3}, bounding_boxes=bbox)
1107    assert model.bounding_box == ((-1, 1), (-3, 3))
1108
1109    model = fix_inputs(base_model, {'x0': 2.5, 'x1': 1.3}, bounding_boxes=bbox,
1110                       selector_args=(('x0', True), ('x1', True)))
1111    assert model.bounding_box == ((-1, 1), (-3, 3))
1112    model = fix_inputs(base_model, {'x0': 2.5, 'x1': 1.3}, bounding_boxes=bbox,
1113                       selector_args=((0, True), (1, True)))
1114    assert model.bounding_box == ((-1, 1), (-3, 3))
1115
1116
1117def test_model_copy_with_bounding_box():
1118    model = models.Polynomial2D(2)
1119    bbox = ModelBoundingBox.validate(model, ((-0.5, 1047.5), (-0.5, 2047.5)), order='F')
1120
1121    # No bbox
1122    model_copy = model.copy()
1123    assert id(model_copy) != id(model)
1124    assert model_copy.get_bounding_box() == model.get_bounding_box() == None
1125
1126    # with bbox
1127    model.bounding_box = bbox
1128    model_copy = model.copy()
1129    assert id(model_copy) != id(model)
1130    assert id(model_copy.bounding_box) != id(model.bounding_box)
1131    for index, interval in model.bounding_box.intervals.items():
1132        interval_copy = model_copy.bounding_box.intervals[index]
1133        assert interval == interval_copy
1134        assert id(interval) != interval_copy
1135
1136    # add model to compound model
1137    model1 = model | models.Identity(1)
1138    model_copy = model1.copy()
1139    assert id(model_copy) != id(model1)
1140    assert model_copy.get_bounding_box() == model1.get_bounding_box() == None
1141
1142
1143def test_compound_model_copy_with_bounding_box():
1144    model = models.Shift(1) & models.Shift(2) & models.Identity(1)
1145    model.inputs = ('x', 'y', 'slit_id')
1146    bbox = ModelBoundingBox.validate(model, ((-0.5, 1047.5), (-0.5, 2047.5), (-np.inf, np.inf)), order='F')
1147
1148    # No bbox
1149    model_copy = model.copy()
1150    assert id(model_copy) != id(model)
1151    assert model_copy.get_bounding_box() == model.get_bounding_box() == None
1152
1153    # with bbox
1154    model.bounding_box = bbox
1155    model_copy = model.copy()
1156    assert id(model_copy) != id(model)
1157    assert id(model_copy.bounding_box) != id(model.bounding_box)
1158    for index, interval in model.bounding_box.intervals.items():
1159        interval_copy = model_copy.bounding_box.intervals[index]
1160        assert interval == interval_copy
1161        assert id(interval) != interval_copy
1162
1163    # add model to compound model
1164    model1 = model | models.Identity(3)
1165    model_copy = model1.copy()
1166    assert id(model_copy) != id(model1)
1167    assert model_copy.get_bounding_box() == model1.get_bounding_box() == None
1168
1169
1170def test_model_copy_with_compound_bounding_box():
1171    model = models.Polynomial2D(2)
1172    bbox = {(0,): (-0.5, 1047.5),
1173            (1,): (-0.5, 3047.5)}
1174    cbbox = CompoundBoundingBox.validate(model, bbox, selector_args=[('x', True)], order='F')
1175
1176    # No cbbox
1177    model_copy = model.copy()
1178    assert id(model_copy) != id(model)
1179    assert model_copy.get_bounding_box() == model.get_bounding_box() == None
1180
1181    # with cbbox
1182    model.bounding_box = cbbox
1183    model_copy = model.copy()
1184    assert id(model_copy) != id(model)
1185    assert id(model_copy.bounding_box) != id(model.bounding_box)
1186    assert model_copy.bounding_box.selector_args == model.bounding_box.selector_args
1187    assert id(model_copy.bounding_box.selector_args) != id(model.bounding_box.selector_args)
1188    for selector, bbox in model.bounding_box.bounding_boxes.items():
1189        for index, interval in bbox.intervals.items():
1190            interval_copy = model_copy.bounding_box.bounding_boxes[selector].intervals[index]
1191            assert interval == interval_copy
1192            assert id(interval) != interval_copy
1193
1194    # add model to compound model
1195    model1 = model | models.Identity(1)
1196    model_copy = model1.copy()
1197    assert id(model_copy) != id(model1)
1198    assert model_copy.get_bounding_box() == model1.get_bounding_box() == None
1199
1200
1201def test_compound_model_copy_with_compound_bounding_box():
1202    model = models.Shift(1) & models.Shift(2) & models.Identity(1)
1203    model.inputs = ('x', 'y', 'slit_id')
1204    bbox = {(0,): ((-0.5, 1047.5), (-0.5, 2047.5)),
1205            (1,): ((-0.5, 3047.5), (-0.5, 4047.5)), }
1206    cbbox = CompoundBoundingBox.validate(model, bbox, selector_args=[('slit_id', True)], order='F')
1207
1208    # No cbbox
1209    model_copy = model.copy()
1210    assert id(model_copy) != id(model)
1211    assert model_copy.get_bounding_box() == model.get_bounding_box() == None
1212
1213    # with cbbox
1214    model.bounding_box = cbbox
1215    model_copy = model.copy()
1216    assert id(model_copy) != id(model)
1217    assert id(model_copy.bounding_box) != id(model.bounding_box)
1218    assert model_copy.bounding_box.selector_args == model.bounding_box.selector_args
1219    assert id(model_copy.bounding_box.selector_args) != id(model.bounding_box.selector_args)
1220    for selector, bbox in model.bounding_box.bounding_boxes.items():
1221        for index, interval in bbox.intervals.items():
1222            interval_copy = model_copy.bounding_box.bounding_boxes[selector].intervals[index]
1223            assert interval == interval_copy
1224            assert id(interval) != interval_copy
1225
1226    # add model to compound model
1227    model1 = model | models.Identity(3)
1228    model_copy = model1.copy()
1229    assert id(model_copy) != id(model1)
1230    assert model_copy.get_bounding_box() == model1.get_bounding_box() == None
1231
1232
1233def test_compound_model_copy_user_attribute():
1234    """Regression test for issue #12370"""
1235
1236    model = models.Gaussian2D(100, 25, 25, 5, 5) | models.Identity(1)
1237    model.xname = 'x_mean'  # user-defined attribute
1238    assert hasattr(model, 'xname')
1239    assert model.xname == 'x_mean'
1240
1241    model_copy = model.copy()
1242    model_copy.xname
1243    assert hasattr(model_copy, 'xname')
1244    assert model_copy.xname == 'x_mean'
1245
1246
1247def test_model_mixed_array_scalar_bounding_box():
1248    """Regression test for issue #12319"""
1249
1250    model = models.Gaussian2D()
1251    bbox = ModelBoundingBox.validate(model, ((-1, 1), (-np.inf, np.inf)), order='F')
1252    model.bounding_box = bbox
1253
1254    x = np.array([-0.5, 0.5])
1255    y = 0
1256
1257    # Everything works when its all in the bounding box
1258    assert (model(x, y) == (model(x, y, with_bounding_box=True))).all()
1259
1260
1261def test_compound_model_mixed_array_scalar_bounding_box():
1262    """Regression test for issue #12319"""
1263
1264    model = models.Shift(1) & models.Shift(2) & models.Identity(1)
1265    model.inputs = ('x', 'y', 'slit_id')
1266    bbox = ModelBoundingBox.validate(model, ((-0.5, 1047.5), (-0.5, 2047.5), (-np.inf, np.inf)), order='F')
1267    model.bounding_box = bbox
1268    x = np.array([1000, 1001])
1269    y = np.array([2000, 2001])
1270    slit_id = 0
1271
1272    # Everything works when its all in the bounding box
1273    value0 = model(x, y, slit_id)
1274    value1 = model(x, y, slit_id, with_bounding_box=True)
1275    assert_equal(value0, value1)
1276
1277
1278def test_model_with_bounding_box_true_and_single_output():
1279    """Regression test for issue #12373"""
1280
1281    model = models.Mapping((1,))
1282    x = [1, 2]
1283    y = [3, 4]
1284
1285    # Check baseline
1286    assert_equal(model(x, y), [3, 4])
1287    # Check with_bounding_box=True should be the same
1288    assert_equal(model(x, y, with_bounding_box=True), [3, 4])
1289
1290    model.bounding_box = ((-np.inf, np.inf), (-np.inf, np.inf))
1291    # Check baseline
1292    assert_equal(model(x, y), [3, 4])
1293    # Check with_bounding_box=True should be the same
1294    assert_equal(model(x, y, with_bounding_box=True), [3, 4])
1295
1296
1297def test_compound_model_with_bounding_box_true_and_single_output():
1298    """Regression test for issue #12373"""
1299
1300    model = models.Mapping((1,)) | models.Shift(1)
1301    x = [1, 2]
1302    y = [3, 4]
1303
1304    # Check baseline
1305    assert_equal(model(x, y), [4, 5])
1306    # Check with_bounding_box=True should be the same
1307    assert_equal(model(x, y, with_bounding_box=True), [4, 5])
1308
1309    model.bounding_box = ((-np.inf, np.inf), (-np.inf, np.inf))
1310    # Check baseline
1311    assert_equal(model(x, y), [4, 5])
1312    # Check with_bounding_box=True should be the same
1313    assert_equal(model(x, y, with_bounding_box=True), [4, 5])
1314