Repository: scipy/scipy

Issue:
DOC: some odr models are missing docs
As discussed in https://github.com/scipy/scipy/pull/12052#discussion_r422497598, some `odr` models missing docs.
Only `odr.polynomial` has doc, because it is a function with docstring, however, other models: `exponential`, `multilinear`, `unilinear`, `quadratic` does not have doc, because these are instances.
Due to this issue, it is difficult for users to know how to use these models.





Discussion:
My possible solution is to change these 4 instances to each factory function with docstring like `polynomial`. Code consistency would be better. 
This goes all the way back to the creation of the `odr` module 2005, in commit 3f92506da. The reason for the difference will be that `polynomial` takes a parameter (`order`), but the other ones are formulas with no parameters. Still, this looks a little weird.

Maybe the right fix here is to change these four `Model` instances to be instances of `Model` subclasses, that can then have their own docstring and doc page in the html docs. This should be a change that's backwards compatible enough. EDIT: analogous to the `stats` distributions.

@rkern any thoughts?
Yeah, that's a plausible workaround.

Resolving patch:
diff --git a/scipy/odr/__init__.py b/scipy/odr/__init__.py
--- a/scipy/odr/__init__.py
+++ b/scipy/odr/__init__.py
@@ -23,16 +23,10 @@
    OdrStop       -- Stop exception.
 
    polynomial    -- Factory function for a general polynomial model.
-
-Prebuilt models:
-
-.. data:: exponential
-
-.. data:: multilinear
-
-.. data:: unilinear
-
-.. data:: quadratic
+   exponential   -- Exponential model
+   multilinear   -- Arbitrary-dimensional linear model
+   unilinear     -- Univariate linear model
+   quadratic     -- Quadratic model
 
 Usage information
 =================
diff --git a/scipy/odr/models.py b/scipy/odr/models.py
--- a/scipy/odr/models.py
+++ b/scipy/odr/models.py
@@ -23,7 +23,7 @@ def _lin_fjb(B, x):
 
 def _lin_fjd(B, x):
     b = B[1:]
-    b = np.repeat(b, (x.shape[-1],)*b.shape[-1],axis=0)
+    b = np.repeat(b, (x.shape[-1],)*b.shape[-1], axis=0)
     b.shape = x.shape
     return b
 
@@ -49,8 +49,8 @@ def _poly_fcn(B, x, powers):
 
 
 def _poly_fjacb(B, x, powers):
-    res = np.concatenate((np.ones(x.shape[-1], float), np.power(x,
-        powers).flat))
+    res = np.concatenate((np.ones(x.shape[-1], float),
+                          np.power(x, powers).flat))
     res.shape = (B.shape[-1], x.shape[-1])
     return res
 
@@ -61,7 +61,7 @@ def _poly_fjacd(B, x, powers):
 
     b = b * powers
 
-    return np.sum(b * np.power(x, powers-1),axis=0)
+    return np.sum(b * np.power(x, powers-1), axis=0)
 
 
 def _exp_fcn(B, x):
@@ -83,11 +83,36 @@ def _exp_est(data):
     return np.array([1., 1.])
 
 
-multilinear = Model(_lin_fcn, fjacb=_lin_fjb,
-               fjacd=_lin_fjd, estimate=_lin_est,
-               meta={'name': 'Arbitrary-dimensional Linear',
-                     'equ':'y = B_0 + Sum[i=1..m, B_i * x_i]',
-                     'TeXequ':r'$y=\beta_0 + \sum_{i=1}^m \beta_i x_i$'})
+class _MultilinearModel(Model):
+    r"""
+    Arbitrary-dimensional linear model
+
+    This model is defined by :math:`y=\beta_0 + \sum_{i=1}^m \beta_i x_i`
+
+    Examples
+    --------
+    We can calculate orthogonal distance regression with an arbitrary
+    dimensional linear model:
+
+    >>> from scipy import odr
+    >>> x = np.linspace(0.0, 5.0)
+    >>> y = 10.0 + 5.0 * x
+    >>> data = odr.Data(x, y)
+    >>> odr_obj = odr.ODR(data, odr.multilinear)
+    >>> output = odr_obj.run()
+    >>> print(output.beta)
+    [10.  5.]
+
+    """
+    def __init__(self):
+        super().__init__(
+            _lin_fcn, fjacb=_lin_fjb, fjacd=_lin_fjd, estimate=_lin_est,
+            meta={'name': 'Arbitrary-dimensional Linear',
+                  'equ': 'y = B_0 + Sum[i=1..m, B_i * x_i]',
+                  'TeXequ': r'$y=\beta_0 + \sum_{i=1}^m \beta_i x_i$'})
+
+
+multilinear = _MultilinearModel()
 
 
 def polynomial(order):
@@ -150,10 +175,35 @@ def _poly_est(data, len_beta=len_beta):
                         (len_beta-1)})
 
 
-exponential = Model(_exp_fcn, fjacd=_exp_fjd, fjacb=_exp_fjb,
-                    estimate=_exp_est, meta={'name':'Exponential',
-                    'equ': 'y= B_0 + exp(B_1 * x)',
-                    'TeXequ': r'$y=\beta_0 + e^{\beta_1 x}$'})
+class _ExponentialModel(Model):
+    r"""
+    Exponential model
+
+    This model is defined by :math:`y=\beta_0 + e^{\beta_1 x}`
+
+    Examples
+    --------
+    We can calculate orthogonal distance regression with an exponential model:
+
+    >>> from scipy import odr
+    >>> x = np.linspace(0.0, 5.0)
+    >>> y = -10.0 + np.exp(0.5*x)
+    >>> data = odr.Data(x, y)
+    >>> odr_obj = odr.ODR(data, odr.exponential)
+    >>> output = odr_obj.run()
+    >>> print(output.beta)
+    [-10.    0.5]
+
+    """
+    def __init__(self):
+        super().__init__(_exp_fcn, fjacd=_exp_fjd, fjacb=_exp_fjb,
+                         estimate=_exp_est,
+                         meta={'name': 'Exponential',
+                               'equ': 'y= B_0 + exp(B_1 * x)',
+                               'TeXequ': r'$y=\beta_0 + e^{\beta_1 x}$'})
+
+
+exponential = _ExponentialModel()
 
 
 def _unilin(B, x):
@@ -194,12 +244,63 @@ def _quad_est(data):
     return (1.,1.,1.)
 
 
-unilinear = Model(_unilin, fjacd=_unilin_fjd, fjacb=_unilin_fjb,
-                  estimate=_unilin_est, meta={'name': 'Univariate Linear',
-                  'equ': 'y = B_0 * x + B_1',
-                  'TeXequ': '$y = \\beta_0 x + \\beta_1$'})
+class _UnilinearModel(Model):
+    r"""
+    Univariate linear model
+
+    This model is defined by :math:`y = \beta_0 x + \beta_1`
 
-quadratic = Model(_quadratic, fjacd=_quad_fjd, fjacb=_quad_fjb,
-                  estimate=_quad_est, meta={'name': 'Quadratic',
+    Examples
+    --------
+    We can calculate orthogonal distance regression with an unilinear model:
+
+    >>> from scipy import odr
+    >>> x = np.linspace(0.0, 5.0)
+    >>> y = 1.0 * x + 2.0
+    >>> data = odr.Data(x, y)
+    >>> odr_obj = odr.ODR(data, odr.unilinear)
+    >>> output = odr_obj.run()
+    >>> print(output.beta)
+    [1. 2.]
+
+    """
+    def __init__(self):
+        super().__init__(_unilin, fjacd=_unilin_fjd, fjacb=_unilin_fjb,
+                         estimate=_unilin_est,
+                         meta={'name': 'Univariate Linear',
+                               'equ': 'y = B_0 * x + B_1',
+                               'TeXequ': '$y = \\beta_0 x + \\beta_1$'})
+
+
+unilinear = _UnilinearModel()
+
+
+class _QuadraticModel(Model):
+    r"""
+    Quadratic model
+
+    This model is defined by :math:`y = \beta_0 x^2 + \beta_1 x + \beta_2`
+
+    Examples
+    --------
+    We can calculate orthogonal distance regression with a quadratic model:
+
+    >>> from scipy import odr
+    >>> x = np.linspace(0.0, 5.0)
+    >>> y = 1.0 * x ** 2 + 2.0 * x + 3.0
+    >>> data = odr.Data(x, y)
+    >>> odr_obj = odr.ODR(data, odr.quadratic)
+    >>> output = odr_obj.run()
+    >>> print(output.beta)
+    [1. 2. 3.]
+
+    """
+    def __init__(self):
+        super().__init__(
+            _quadratic, fjacd=_quad_fjd, fjacb=_quad_fjb, estimate=_quad_est,
+            meta={'name': 'Quadratic',
                   'equ': 'y = B_0*x**2 + B_1*x + B_2',
                   'TeXequ': '$y = \\beta_0 x^2 + \\beta_1 x + \\beta_2'})
+
+
+quadratic = _QuadraticModel()


Test patch:
diff --git a/scipy/odr/tests/test_odr.py b/scipy/odr/tests/test_odr.py
--- a/scipy/odr/tests/test_odr.py
+++ b/scipy/odr/tests/test_odr.py
@@ -4,7 +4,9 @@
 from numpy.testing import (assert_array_almost_equal,
                            assert_equal, assert_warns)
 from pytest import raises as assert_raises
-from scipy.odr import Data, Model, ODR, RealData, OdrStop, OdrWarning
+from scipy.odr import (Data, Model, ODR, RealData, OdrStop, OdrWarning,
+                       multilinear, exponential, unilinear, quadratic,
+                       polynomial)
 
 
 class TestODR(object):
@@ -428,3 +430,45 @@ def df_dx_odr(beta, x):
         # check results
         assert_equal(odr_out.info, 1)
         assert_array_almost_equal(odr_out.beta, beta_solution)
+
+    def test_multilinear_model(self):
+        x = np.linspace(0.0, 5.0)
+        y = 10.0 + 5.0 * x
+        data = Data(x, y)
+        odr_obj = ODR(data, multilinear)
+        output = odr_obj.run()
+        assert_array_almost_equal(output.beta, [10.0, 5.0])
+
+    def test_exponential_model(self):
+        x = np.linspace(0.0, 5.0)
+        y = -10.0 + np.exp(0.5*x)
+        data = Data(x, y)
+        odr_obj = ODR(data, exponential)
+        output = odr_obj.run()
+        assert_array_almost_equal(output.beta, [-10.0, 0.5])
+
+    def test_polynomial_model(self):
+        x = np.linspace(0.0, 5.0)
+        y = 1.0 + 2.0 * x + 3.0 * x ** 2 + 4.0 * x ** 3
+        poly_model = polynomial(3)
+        data = Data(x, y)
+        odr_obj = ODR(data, poly_model)
+        output = odr_obj.run()
+        assert_array_almost_equal(output.beta, [1.0, 2.0, 3.0, 4.0])
+
+    def test_unilinear_model(self):
+        x = np.linspace(0.0, 5.0)
+        y = 1.0 * x + 2.0
+        data = Data(x, y)
+        odr_obj = ODR(data, unilinear)
+        output = odr_obj.run()
+        assert_array_almost_equal(output.beta, [1.0, 2.0])
+
+    def test_quadratic_model(self):
+        x = np.linspace(0.0, 5.0)
+        y = 1.0 * x ** 2 + 2.0 * x + 3.0
+        data = Data(x, y)
+        odr_obj = ODR(data, quadratic)
+        output = odr_obj.run()
+        assert_array_almost_equal(output.beta, [1.0, 2.0, 3.0])
+