# Licensed under a 3-clause BSD style license - see LICENSE.rst
# pylint: disable=invalid-name, no-member

import pytest
import numpy as np

from astropy import units as u
from astropy.tests.helper import assert_quantity_allclose
from astropy.utils.compat.optional_deps import HAS_SCIPY  # noqa
from astropy.modeling.core import fix_inputs

from astropy.modeling.functional_models import (
    Gaussian1D, Sersic1D,
    Sine1D, Cosine1D, Tangent1D, ArcSine1D, ArcCosine1D, ArcTangent1D,
    Linear1D, Lorentz1D, Voigt1D, Const1D,
    Box1D, Trapezoid1D, RickerWavelet1D,
    Moffat1D, Gaussian2D, Const2D, Ellipse2D,
    Disk2D, Ring2D, Box2D, TrapezoidDisk2D,
    RickerWavelet2D, AiryDisk2D, Moffat2D, Sersic2D,
    KingProjectedAnalytic1D,
    Scale, Multiply,
    Planar2D, Logarithmic1D, Exponential1D)

from astropy.modeling.physical_models import Plummer1D, Drude1D

from astropy.modeling.powerlaws import (
    PowerLaw1D, BrokenPowerLaw1D, SmoothlyBrokenPowerLaw1D,
    ExponentialCutoffPowerLaw1D, LogParabola1D)

from astropy.modeling.polynomial import Polynomial1D, Polynomial2D

from astropy.modeling.fitting import LevMarLSQFitter
from astropy.modeling.bounding_box import ModelBoundingBox

from astropy.modeling.parameters import InputParameterError

FUNC_MODELS_1D = [
{'class': Gaussian1D,
 'parameters': {'amplitude': 3 * u.Jy, 'mean': 2 * u.m, 'stddev': 30 * u.cm},
 'evaluation': [(2600 * u.mm, 3 * u.Jy * np.exp(-2))],
 'bounding_box': [0.35, 3.65] * u.m},
{'class': Sersic1D,
 'parameters': {'amplitude': 3 * u.MJy / u.sr, 'r_eff': 2 * u.arcsec, 'n': 4},
 'evaluation': [(3 * u.arcsec, 1.3237148119468918 * u.MJy/u.sr)],
 'bounding_box': False},
{'class': Sine1D,
 'parameters': {'amplitude': 3 * u.km / u.s, 'frequency': 0.25 * u.Hz, 'phase': 0.5},
 'evaluation': [(1 * u.s, -3 * u.km / u.s)],
 'bounding_box': False},
{'class': Cosine1D,
 'parameters': {'amplitude': 3 * u.km / u.s, 'frequency': 0.25 * u.Hz, 'phase': 0.25},
 'evaluation': [(1 * u.s, -3 * u.km / u.s)],
 'bounding_box': False},
{'class': Tangent1D,
 'parameters': {'amplitude': 3 * u.km / u.s, 'frequency': 0.125 * u.Hz, 'phase': 0.25},
 'evaluation': [(1 * u.s, -3 * u.km / u.s)],
 'bounding_box': [-4, 0] / u.Hz},
{'class': ArcSine1D,
 'parameters': {'amplitude': 3 * u.km / u.s, 'frequency': 0.25 * u.Hz, 'phase': 0.5},
 'evaluation': [(0 * u.km / u.s, -2 * u.s)],
 'bounding_box': [-3, 3] * u.km / u.s},
{'class': ArcCosine1D,
 'parameters': {'amplitude': 3 * u.km / u.s, 'frequency': 0.25 * u.Hz, 'phase': 0.5},
 'evaluation': [(0 * u.km / u.s, -1 * u.s)],
 'bounding_box': [-3, 3] * u.km / u.s},
{'class': ArcTangent1D,
 'parameters': {'amplitude': 3 * u.km / u.s, 'frequency': 0.125 * u.Hz, 'phase': 0.25},
 'evaluation': [(0 * u.km / u.s, -2 * u.s)],
 'bounding_box': False},
{'class': Linear1D,
 'parameters': {'slope': 3 * u.km / u.s, 'intercept': 5000 * u.m},
 'evaluation': [(6000 * u.ms, 23 * u.km)],
 'bounding_box': False},
{'class': Lorentz1D,
 'parameters': {'amplitude': 2 * u.Jy, 'x_0': 505 * u.nm, 'fwhm': 100 * u.AA},
 'evaluation': [(0.51 * u.micron, 1 * u.Jy)],
 'bounding_box': [255, 755] * u.nm},
{'class': Voigt1D,
 'parameters': {'amplitude_L': 2 * u.Jy, 'x_0': 505 * u.nm,
                'fwhm_L': 100 * u.AA, 'fwhm_G': 50 * u.AA},
 'evaluation': [(0.51 * u.micron, 1.0621795524 * u.Jy)],
 'bounding_box': False},
{'class': Const1D,
 'parameters': {'amplitude': 3 * u.Jy},
 'evaluation': [(0.6 * u.micron, 3 * u.Jy)],
 'bounding_box': False},
{'class': Box1D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 4.4 * u.um, 'width': 1 * u.um},
 'evaluation': [(4200 * u.nm, 3 * u.Jy), (1 * u.m, 0 * u.Jy)],
 'bounding_box': [3.9, 4.9] * u.um},
{'class': Trapezoid1D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 4.4 * u.um, 'width': 1 * u.um, 'slope': 5 * u.Jy / u.um},
 'evaluation': [(4200 * u.nm, 3 * u.Jy), (1 * u.m, 0 * u.Jy)],
 'bounding_box': [3.3, 5.5] * u.um},
{'class': RickerWavelet1D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 4.4 * u.um, 'sigma': 1e-3 * u.mm},
 'evaluation': [(1000 * u.nm, -0.09785050 * u.Jy)],
 'bounding_box': [-5.6, 14.4] * u.um},
{'class': Moffat1D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 4.4 * u.um, 'gamma': 1e-3 * u.mm, 'alpha': 1},
 'evaluation': [(1000 * u.nm, 0.238853503 * u.Jy)],
 'bounding_box': False},
{'class': KingProjectedAnalytic1D,
 'parameters': {'amplitude': 1. * u.Msun/u.pc**2, 'r_core': 1. * u.pc, 'r_tide': 2. * u.pc},
 'evaluation': [(0.5 * u.pc, 0.2 * u.Msun/u.pc**2)],
 'bounding_box': [0. * u.pc, 2. * u.pc]},
{'class': Logarithmic1D,
 'parameters': {'amplitude': 5*u.m, 'tau': 2 * u.m},
 'evaluation': [(4 * u.m, 3.4657359027997265 * u.m)],
 'bounding_box': False},
{'class': Exponential1D,
 'parameters': {'amplitude': 5*u.m, 'tau': 2 * u.m},
 'evaluation': [(4 * u.m, 36.945280494653254 * u.m)],
 'bounding_box': False}
 ]

SCALE_MODELS = [
{'class': Scale,
 'parameters': {'factor': 2*u.m},
 'evaluation': [(1*u.m, 2*u.m)],
 'bounding_box': False},
{'class': Multiply,
 'parameters': {'factor': 2*u.m},
 'evaluation': [(1 * u.m/u.m, 2*u.m)],
 'bounding_box': False},
]

PHYS_MODELS_1D = [
{'class': Plummer1D,
 'parameters': {'mass': 3 * u.kg, 'r_plum': 0.5 * u.m},
 'evaluation': [(1* u.m, 0.10249381 * u.kg / (u.m **3))],
 'bounding_box': False},
{'class': Drude1D,
 'parameters': {'amplitude': 1.0 * u.m, 'x_0': 2175. * u.AA, 'fwhm': 400. * u.AA},
 'evaluation': [(2000*u.AA, 0.5452317018423869 * u.m)],
 'bounding_box': [-17825, 22175] * u.AA},
 ]

FUNC_MODELS_2D = [
{'class': Gaussian2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_mean': 2 * u.m, 'y_mean': 1 * u.m,
                'x_stddev': 3 * u.m, 'y_stddev': 2 * u.m, 'theta': 45 * u.deg},
 'evaluation': [(412.1320343 * u.cm, 3.121320343 * u.m, 3 * u.Jy * np.exp(-0.5))],
 'bounding_box': [[-13.02230366, 15.02230366],
                  [-12.02230366, 16.02230366]] * u.m},
{'class': Const2D,
 'parameters': {'amplitude': 3 * u.Jy},
 'evaluation': [(0.6 * u.micron, 0.2 * u.m, 3 * u.Jy)],
 'bounding_box': False},
{'class': Disk2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                'R_0': 300 * u.cm},
 'evaluation': [(5.8 * u.m, 201 * u.cm, 3 * u.Jy)],
 'bounding_box': [[-1, 5], [0, 6]] * u.m},
{'class': TrapezoidDisk2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 1 * u.m, 'y_0': 2 * u.m,
                'R_0': 100 * u.cm, 'slope': 1 * u.Jy / u.m},
 'evaluation': [(3.5 * u.m, 2 * u.m, 1.5 * u.Jy)],
 'bounding_box': [[-2, 6], [-3, 5]] * u.m},
{'class': Ellipse2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                'a': 300 * u.cm, 'b': 200 * u.cm, 'theta': 45 * u.deg},
 'evaluation': [(4 * u.m, 300 * u.cm, 3 * u.Jy)],
 'bounding_box': [[-0.76046808, 4.76046808], [0.68055697, 5.31944302]] * u.m},
{'class': Ring2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                'r_in': 2 * u.cm, 'r_out': 2.1 * u.cm},
 'evaluation': [(302.05 * u.cm, 2 * u.m + 10 * u.um, 3 * u.Jy)],
 'bounding_box': [[1.979, 2.021], [2.979, 3.021]] * u.m},
{'class': Box2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.s,
                'x_width': 4 * u.cm, 'y_width': 3 * u.s},
 'evaluation': [(301 * u.cm, 3 * u.s, 3 * u.Jy)],
 'bounding_box': [[0.5 * u.s, 3.5 * u.s], [2.98 * u.m, 3.02 * u.m]]},
{'class': RickerWavelet2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                'sigma': 1 * u.m},
 'evaluation': [(4 * u.m, 2.5 * u.m, 0.602169107 * u.Jy)],
 'bounding_box': False},
{'class': AiryDisk2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                'radius': 1 * u.m},
 'evaluation': [(4 * u.m, 2.1 * u.m, 4.76998480e-05 * u.Jy)],
 'bounding_box': False},
{'class': Moffat2D,
 'parameters': {'amplitude': 3 * u.Jy, 'x_0': 4.4 * u.um, 'y_0': 3.5 * u.um,
                'gamma': 1e-3 * u.mm, 'alpha': 1},
 'evaluation': [(1000 * u.nm, 2 * u.um, 0.202565833 * u.Jy)],
 'bounding_box': False},
{'class': Sersic2D,
 'parameters': {'amplitude': 3 * u.MJy / u.sr, 'x_0': 1 * u.arcsec,
                'y_0': 2 * u.arcsec, 'r_eff': 2 * u.arcsec, 'n': 4,
                'ellip': 0, 'theta': 0},
 'evaluation': [(3 * u.arcsec, 2.5 * u.arcsec, 2.829990489 * u.MJy/u.sr)],
 'bounding_box': False},
{'class': Planar2D,
 'parameters': {'slope_x': 2*u.m, 'slope_y': 3*u.m, 'intercept': 4*u.m},
 'evaluation': [(5*u.m/u.m, 6*u.m/u.m, 32*u.m)],
 'bounding_box': False},
]

POWERLAW_MODELS = [
{'class': PowerLaw1D,
 'parameters': {'amplitude': 5 * u.kg, 'x_0': 10 * u.cm, 'alpha': 1},
 'evaluation': [(1 * u.m, 500 * u.g)],
 'bounding_box': False},
{'class': BrokenPowerLaw1D,
 'parameters': {'amplitude': 5 * u.kg, 'x_break': 10 * u.cm, 'alpha_1': 1, 'alpha_2': -1},
 'evaluation': [(1 * u.m, 50 * u.kg), (1 * u.cm, 50 * u.kg)],
 'bounding_box': False},
{'class': SmoothlyBrokenPowerLaw1D,
 'parameters': {'amplitude': 5 * u.kg, 'x_break': 10 * u.cm, 'alpha_1': 1, 'alpha_2': -1, 'delta': 1},
 'evaluation': [(1 * u.cm, 15.125 * u.kg), (1 * u.m, 15.125 * u.kg)],
 'bounding_box': False},
{'class': ExponentialCutoffPowerLaw1D,
 'parameters': {'amplitude': 5 * u.kg, 'x_0': 10 * u.cm, 'alpha': 1, 'x_cutoff': 1 * u.m},
 'evaluation': [(1 * u.um, 499999.5 * u.kg), (10 * u.m, 50 * np.exp(-10) * u.g)],
 'bounding_box': False},
{'class': LogParabola1D,
 'parameters': {'amplitude': 5 * u.kg, 'x_0': 10 * u.cm, 'alpha': 1, 'beta': 2},
 'evaluation': [(1 * u.cm, 5 * 0.1 ** (-1 - 2 * np.log(0.1)) * u.kg)],
 'bounding_box': False}
]

POLY_MODELS = [
{'class': Polynomial1D,
 'parameters': {'degree': 2, 'c0': 3 * u.one, 'c1': 2 / u.m, 'c2': 3 / u.m**2},
 'evaluation': [(3 * u.m, 36 * u.one)],
 'bounding_box': False},
{'class': Polynomial1D,
 'parameters': {'degree': 2, 'c0': 3 * u.kg, 'c1': 2 * u.kg / u.m, 'c2': 3 * u.kg / u.m**2},
 'evaluation': [(3 * u.m, 36 * u.kg)],
 'bounding_box': False},
{'class': Polynomial1D,
 'parameters': {'degree': 2, 'c0': 3 * u.kg, 'c1': 2 * u.kg, 'c2': 3 * u.kg},
 'evaluation': [(3 * u.one, 36 * u.kg)],
 'bounding_box': False},
{'class': Polynomial2D,
 'parameters': {'degree': 2, 'c0_0': 3 * u.one, 'c1_0': 2 / u.m, 'c2_0': 3 / u.m**2,
                'c0_1': 3 / u.s, 'c0_2': -2 / u.s**2, 'c1_1': 5 / u.m / u.s},
 'evaluation': [(3 * u.m, 2 * u.s, 64 * u.one)],
 'bounding_box': False},
{'class': Polynomial2D,
 'parameters': {'degree': 2, 'c0_0': 3 * u.kg, 'c1_0': 2 * u.kg / u.m, 'c2_0': 3 * u.kg / u.m**2,
                'c0_1': 3 * u.kg / u.s, 'c0_2': -2 * u.kg / u.s**2, 'c1_1': 5 * u.kg / u.m / u.s},
 'evaluation': [(3 * u.m, 2 * u.s, 64 * u.kg)],
 'bounding_box': False},
{'class': Polynomial2D,
 'parameters': {'degree': 2, 'c0_0': 3 * u.kg, 'c1_0': 2 * u.kg, 'c2_0': 3 * u.kg,
                'c0_1': 3 * u.kg, 'c0_2': -2 * u.kg, 'c1_1': 5 * u.kg},
 'evaluation': [(3 * u.one, 2 * u.one, 64 * u.kg)],
 'bounding_box': False},
 ]


MODELS = FUNC_MODELS_1D + SCALE_MODELS + FUNC_MODELS_2D + POWERLAW_MODELS +\
    PHYS_MODELS_1D + POLY_MODELS

SCIPY_MODELS = set([Sersic1D, Sersic2D, AiryDisk2D])


@pytest.mark.parametrize('model', MODELS)
def test_models_evaluate_without_units(model):
    if not HAS_SCIPY and model['class'] in SCIPY_MODELS:
        pytest.skip()
    m = model['class'](**model['parameters'])
    for args in model['evaluation']:
        if len(args) == 2:
            kwargs = dict(zip(('x', 'y'), args))
        else:
            kwargs = dict(zip(('x', 'y', 'z'), args))
            if kwargs['x'].unit.is_equivalent(kwargs['y'].unit):
                kwargs['x'] = kwargs['x'].to(kwargs['y'].unit)
        mnu = m.without_units_for_data(**kwargs)
        args = [x.value for x in kwargs.values()]
        assert_quantity_allclose(mnu(*args[:-1]), args[-1])


@pytest.mark.parametrize('model', MODELS)
def test_models_evaluate_with_units(model):
    if not HAS_SCIPY and model['class'] in SCIPY_MODELS:
        pytest.skip()
    m = model['class'](**model['parameters'])
    for args in model['evaluation']:
        assert_quantity_allclose(m(*args[:-1]), args[-1])


@pytest.mark.parametrize('model', MODELS)
def test_models_evaluate_with_units_x_array(model):

    if not HAS_SCIPY and model['class'] in SCIPY_MODELS:
        pytest.skip()

    m = model['class'](**model['parameters'])

    for args in model['evaluation']:
        if len(args) == 2:
            x, y = args
            x_arr = u.Quantity([x, x])
            result = m(x_arr)
            assert_quantity_allclose(result, u.Quantity([y, y]))
        else:
            x, y, z = args
            x_arr = u.Quantity([x, x])
            y_arr = u.Quantity([y, y])
            result = m(x_arr, y_arr)
            assert_quantity_allclose(result, u.Quantity([z, z]))


@pytest.mark.parametrize('model', MODELS)
def test_models_evaluate_with_units_param_array(model):

    if not HAS_SCIPY and model['class'] in SCIPY_MODELS:
        pytest.skip()

    params = {}
    for key, value in model['parameters'].items():
        if value is None or key == 'degree':
            params[key] = value
        else:
            params[key] = np.repeat(value, 2)

    params['n_models'] = 2

    m = model['class'](**params)

    for args in model['evaluation']:
        if len(args) == 2:
            x, y = args
            x_arr = u.Quantity([x, x])
            result = m(x_arr)
            assert_quantity_allclose(result, u.Quantity([y, y]))
        else:
            x, y, z = args
            x_arr = u.Quantity([x, x])
            y_arr = u.Quantity([y, y])
            result = m(x_arr, y_arr)
            assert_quantity_allclose(result, u.Quantity([z, z]))

    if model['class'] == Drude1D:
        params['x_0'][-1] = 0 * u.AA
        with pytest.raises(InputParameterError) as err:
            model['class'](**params)
        assert str(err.value) ==\
            '0 is not an allowed value for x_0'


@pytest.mark.parametrize('model', MODELS)
def test_models_bounding_box(model):

    # In some cases, having units in parameters caused bounding_box to break,
    # so this is to ensure that it works correctly.

    if not HAS_SCIPY and model['class'] in SCIPY_MODELS:
        pytest.skip()

    m = model['class'](**model['parameters'])

    # In the following we need to explicitly test that the value is False
    # since Quantities no longer evaluate as as True
    if model['bounding_box'] is False:
        # Check that NotImplementedError is raised, so that if bounding_box is
        # implemented we remember to set bounding_box=True in the list of models
        # above
        with pytest.raises(NotImplementedError):
            m.bounding_box
    else:
        # A bounding box may have inhomogeneous units so we need to check the
        # values one by one.
        for i in range(len(model['bounding_box'])):
            bbox = m.bounding_box
            if isinstance(bbox, ModelBoundingBox):
                bbox = bbox.bounding_box()
            assert_quantity_allclose(bbox[i], model['bounding_box'][i])


@pytest.mark.parametrize('model', MODELS)
def test_compound_model_input_units_equivalencies_defaults(model):
    m = model['class'](**model['parameters'])

    assert m.input_units_equivalencies is None

    compound_model = m + m
    assert compound_model.inputs_map()['x'][0].input_units_equivalencies is None
    fixed_input_model = fix_inputs(compound_model, {'x':1})

    assert fixed_input_model.input_units_equivalencies is None

    compound_model = m - m
    assert compound_model.inputs_map()['x'][0].input_units_equivalencies is None
    fixed_input_model = fix_inputs(compound_model, {'x':1})

    assert fixed_input_model.input_units_equivalencies is None

    compound_model = m & m
    assert compound_model.inputs_map()['x1'][0].input_units_equivalencies is None
    fixed_input_model = fix_inputs(compound_model, {'x0':1})
    assert fixed_input_model.inputs_map()['x1'][0].input_units_equivalencies is None

    assert fixed_input_model.input_units_equivalencies is None

    if m.n_outputs == m.n_inputs:
        compound_model = m | m
        assert compound_model.inputs_map()['x'][0].input_units_equivalencies is None
        fixed_input_model = fix_inputs(compound_model, {'x':1})

        assert fixed_input_model.input_units_equivalencies is None


@pytest.mark.skipif('not HAS_SCIPY')
@pytest.mark.filterwarnings(r'ignore:.*:RuntimeWarning')
@pytest.mark.filterwarnings(r'ignore:Model is linear in parameters.*')
@pytest.mark.filterwarnings(r'ignore:The fit may be unsuccessful.*')
@pytest.mark.parametrize('model', MODELS)
def test_models_fitting(model):

    m = model['class'](**model['parameters'])
    if len(model['evaluation'][0]) == 2:
        x = np.linspace(1, 3, 100) * model['evaluation'][0][0].unit
        y = np.exp(-x.value ** 2) * model['evaluation'][0][1].unit
        args = [x, y]
    else:
        x = np.linspace(1, 3, 100) * model['evaluation'][0][0].unit
        y = np.linspace(1, 3, 100) * model['evaluation'][0][1].unit
        z = np.exp(-x.value**2 - y.value**2) * model['evaluation'][0][2].unit
        args = [x, y, z]

    # Test that the model fits even if it has units on parameters
    fitter = LevMarLSQFitter()
    m_new = fitter(m, *args)

    # Check that units have been put back correctly
    for param_name in m.param_names:
        par_bef = getattr(m, param_name)
        par_aft = getattr(m_new, param_name)
        if par_bef.unit is None:
            # If the parameter used to not have a unit then had a radian unit
            # for example, then we should allow that
            assert par_aft.unit is None or par_aft.unit is u.rad
        else:
            assert par_aft.unit.is_equivalent(par_bef.unit)


unit_mismatch_models = [
    {'class': Gaussian2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_mean': 2 * u.m, 'y_mean': 1 * u.m,
                    'x_stddev': 3 * u.m, 'y_stddev': 2 * u.m, 'theta': 45 * u.deg},
     'evaluation': [(412.1320343 * u.cm, 3.121320343 * u.K, 3 * u.Jy * np.exp(-0.5)),
                    (412.1320343 * u.K, 3.121320343 * u.m, 3 * u.Jy * np.exp(-0.5))],
     'bounding_box': [[-14.18257445, 16.18257445], [-10.75693665, 14.75693665]] * u.m},
    {'class': Ellipse2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                    'a': 300 * u.cm, 'b': 200 * u.cm, 'theta': 45 * u.deg},
     'evaluation': [(4 * u.m, 300 * u.K, 3 * u.Jy),
                    (4 * u.K, 300 * u.cm, 3 * u.Jy)],
     'bounding_box': [[-0.76046808, 4.76046808], [0.68055697, 5.31944302]] * u.m},
    {'class': Disk2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                    'R_0': 300 * u.cm},
     'evaluation': [(5.8 * u.m, 201 * u.K, 3 * u.Jy),
                    (5.8 * u.K, 201 * u.cm, 3 * u.Jy)],
     'bounding_box': [[-1, 5], [0, 6]] * u.m},
    {'class': Ring2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                    'r_in': 2 * u.cm, 'r_out': 2.1 * u.cm},
     'evaluation': [(302.05 * u.cm, 2 * u.K + 10 * u.K, 3 * u.Jy),
                    (302.05 * u.K, 2 * u.m + 10 * u.um, 3 * u.Jy)],
     'bounding_box': [[1.979, 2.021], [2.979, 3.021]] * u.m},
    {'class': TrapezoidDisk2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 1 * u.m, 'y_0': 2 * u.m,
                    'R_0': 100 * u.cm, 'slope': 1 * u.Jy / u.m},
     'evaluation': [(3.5 * u.m, 2 * u.K, 1.5 * u.Jy),
                    (3.5 * u.K, 2 * u.m, 1.5 * u.Jy)],
     'bounding_box': [[-2, 6], [-3, 5]] * u.m},
    {'class': RickerWavelet2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                    'sigma': 1 * u.m},
     'evaluation': [(4 * u.m, 2.5 * u.K, 0.602169107 * u.Jy),
                    (4 * u.K, 2.5 * u.m, 0.602169107 * u.Jy)],
     'bounding_box': False},
    {'class': AiryDisk2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 3 * u.m, 'y_0': 2 * u.m,
                    'radius': 1 * u.m},
     'evaluation': [(4 * u.m, 2.1 * u.K, 4.76998480e-05 * u.Jy),
                    (4 * u.K, 2.1 * u.m, 4.76998480e-05 * u.Jy)],
     'bounding_box': False},
    {'class': Moffat2D,
     'parameters': {'amplitude': 3 * u.Jy, 'x_0': 4.4 * u.um, 'y_0': 3.5 * u.um,
                    'gamma': 1e-3 * u.mm, 'alpha': 1},
     'evaluation': [(1000 * u.nm, 2 * u.K, 0.202565833 * u.Jy),
                    (1000 * u.K, 2 * u.um, 0.202565833 * u.Jy)],
     'bounding_box': False},
    {'class': Sersic2D,
     'parameters': {'amplitude': 3 * u.MJy / u.sr, 'x_0': 1 * u.arcsec,
                    'y_0': 2 * u.arcsec, 'r_eff': 2 * u.arcsec, 'n': 4,
                    'ellip': 0, 'theta': 0},
     'evaluation': [(3 * u.arcsec, 2.5 * u.m, 2.829990489 * u.MJy/u.sr),
                    (3 * u.m, 2.5 * u.arcsec, 2.829990489 * u.MJy/u.sr)],
     'bounding_box': False},
]


@pytest.mark.parametrize('model', unit_mismatch_models)
def test_input_unit_mismatch_error(model):
    if not HAS_SCIPY and model['class'] in SCIPY_MODELS:
        pytest.skip()

    message = "Units of 'x' and 'y' inputs should match"

    m = model['class'](**model['parameters'])

    for args in model['evaluation']:
        if len(args) == 2:
            kwargs = dict(zip(('x', 'y'), args))
        else:
            kwargs = dict(zip(('x', 'y', 'z'), args))
            if kwargs['x'].unit.is_equivalent(kwargs['y'].unit):
                kwargs['x'] = kwargs['x'].to(kwargs['y'].unit)
        with pytest.raises(u.UnitsError) as err:
            m.without_units_for_data(**kwargs)
        assert str(err.value) == message
