```python
import numpy as np
import matplotlib.pyplot as plt
import ttim as tt
import timml as tm
```

### Example 4
Aquifer storage and recovery

```python
# parameters
Q = 100 # discharge of well, m^3/d
k = 10 # hydraulic conductivity, m/d
H = 10 # thickness of aquifer, m
Ss = 1e-4 # specific storage, m^(-1)
npor = 0.1 # porosity, -
xw = 0 # x-location of well
yw = 0 # y-location of well
rw = 0.01 # radius of well
```

```python
ml = tt.ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], poraq=[npor], tmin=0.001, tmax=1000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, -Q)], rw=rw)
ml.solve()
```

    self.neq  1
    solution complete

```python
ml = tt.ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], poraq=[npor], tmin=0.001, tmax=1000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, -Q), (100, Q)], rw=rw)
ml.solve()
```

    self.neq  1
    solution complete

```python
ml.head(10, 0, 100 - 1e-6)
```

    array([[0.98055413]])

```python
ml.head(10, 0, 100 + 1e-3)
```

    array([[0.8143499]])

```python
t = np.logspace(-3, 2, 100)
h = ml.head(10, 0, t)
plt.semilogx(t, h[0])
```

```python
ml.head(10, 0, [90, 100, 110])
```

    array([[ 0.97216983,  0.98055414, -0.60650735]])

```python
trace = tt.timtrace(ml, xstart=rw, ystart=0, zstart=0.5 * H, tstartend=[0, 100],
                    tstartoffset=0.001, tstep=0.1, hstepmax=2, nstepmax=2000)
```

    reached maximum time tmax

```python
xyzt = trace['xyzt']
plt.plot(xyzt[:, 3], xyzt[:, 0])
```

```python
ml = tt.ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], poraq=[npor], tmin=0.001, tmax=1000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, -Q), (100, Q)], rw=rw)
ml.solve()
trace = tt.timtrace(ml, xstart=rw, ystart=0, zstart=0.5 * H, tstartend=[0, 0.1, 1, 100],
                    tstartoffset=0.001, tstep=[0.01, 0.1, 1], hstepmax=2, nstepmax=2000)
```

    self.neq  1
    solution complete
    reached maximum time tmax
    reached maximum time tmax
    reached maximum time tmax

```python
xend, yend, zend, tend = trace['xyzt'][-1]
```

```python
def integrand(r, t=100 - 1e-6):
    h = ml.head(r, 0, t)
    return h[0, 0] * 2 * np.pi * r * ml.aq.Saq[0]
```

```python
ml.aq.Saq
```

    array([0.0001])

```python
from scipy.integrate import quad
np.pi * xend ** 2 * ml.aq.Haq[0] * ml.aq.poraq[0] + quad(integrand, rw, xend)[0]
```

    10007.366405521809

```python
np.pi * rw ** 2 * ml.aq.Haq[0] * ml.aq.poraq[0]
```

    0.0003141592653589793

```python
ml.tintervals
```

    array([1.e-03, 1.e-02, 1.e-01, 1.e+00, 1.e+01, 1.e+02, 1.e+03])

```python
r = np.logspace(-2, 1, 100)
h = ml.headalongline(r, 0, t=100 - 1e-6)
Qx = [ml.disvec(x, 0, t=100 - 1e-6)[0][0, 0] for x in r]
plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.plot(r, h[0, 0])
plt.subplot(122)
plt.loglog(r, Qx)
plt.grid()
```

```python
from scipy.integrate import solve_ivp
solve_ivp??
```

    Signature:
    solve_ivp(
        fun,
        t_span,
        y0,
        method='RK45',
        t_eval=None,
        dense_output=False,
        events=None,
        vectorized=False,
        args=None,
        **options,
    )
    Source:
    def solve_ivp(fun, t_span, y0, method='RK45', t_eval=None, dense_output=False,
                  events=None, vectorized=False, args=None, **options):
        """Solve an initial value problem for a system of ODEs.

        This function numerically integrates a system of ordinary differential
        equations given an initial value::

            dy / dt = f(t, y)
            y(t0) = y0

        Here t is a 1-D independent variable (time), y(t) is an
        N-D vector-valued function (state), and an N-D
        vector-valued function f(t, y) determines the differential equations.
        The goal is to find y(t) approximately satisfying the differential
        equations, given an initial value y(t0)=y0.

        Some of the solvers support integration in the complex domain, but note
        that for stiff ODE solvers, the right-hand side must be
        complex-differentiable (satisfy Cauchy-Riemann equations [11]_).
        To solve a problem in the complex domain, pass y0 with a complex data type.
        Another option always available is to rewrite your problem for real and
        imaginary parts separately.

        Parameters
        ----------
        fun : callable
            Right-hand side of the system. The calling signature is ``fun(t, y)``.
            Here `t` is a scalar, and there are two options for the ndarray `y`:
            It can either have shape (n,); then `fun` must return array_like with
            shape (n,). Alternatively, it can have shape (n, k); then `fun`
            must return an array_like with shape (n, k), i.e., each column
            corresponds to a single column in `y`. The choice between the two
            options is determined by `vectorized` argument (see below). The
            vectorized implementation allows a faster approximation of the Jacobian
            by finite differences (required for stiff solvers).
        t_span : 2-tuple of floats
            Interval of integration (t0, tf). The solver starts with t=t0 and
            integrates until it reaches t=tf.
        y0 : array_like, shape (n,)
            Initial state. For problems in the complex domain, pass `y0` with a
            complex data type (even if the initial value is purely real).
        method : string or `OdeSolver`, optional
            Integration method to use:

                * 'RK45' (default): Explicit Runge-Kutta method of order 5(4) [1]_.
                  The error is controlled assuming accuracy of the fourth-order
                  method, but steps are taken using the fifth-order accurate
                  formula (local extrapolation is done). A quartic interpolation
                  polynomial is used for the dense output [2]_. Can be applied in
                  the complex domain.
                * 'RK23': Explicit Runge-Kutta method of order 3(2) [3]_. The error
                  is controlled assuming accuracy of the second-order method, but
                  steps are taken using the third-order accurate formula (local
                  extrapolation is done). A cubic Hermite polynomial is used for the
                  dense output. Can be applied in the complex domain.
                * 'DOP853': Explicit Runge-Kutta method of order 8 [13]_.
                  Python implementation of the "DOP853" algorithm originally
                  written in Fortran [14]_. A 7-th order interpolation polynomial
                  accurate to 7-th order is used for the dense output.
                  Can be applied in the complex domain.
                * 'Radau': Implicit Runge-Kutta method of the Radau IIA family of
                  order 5 [4]_. The error is controlled with a third-order accurate
                  embedded formula. A cubic polynomial which satisfies the
                  collocation conditions is used for the dense output.
                * 'BDF': Implicit multi-step variable-order (1 to 5) method based
                  on a backward differentiation formula for the derivative
                  approximation [5]_. The implementation follows the one described
                  in [6]_. A quasi-constant step scheme is used and accuracy is
                  enhanced using the NDF modification. Can be applied in the
                  complex domain.
                * 'LSODA': Adams/BDF method with automatic stiffness detection and
                  switching [7]_, [8]_. This is a wrapper of the Fortran solver
                  from ODEPACK.

            Explicit Runge-Kutta methods ('RK23', 'RK45', 'DOP853') should be used
            for non-stiff problems and implicit methods ('Radau', 'BDF') for
            stiff problems [9]_. Among Runge-Kutta methods, 'DOP853' is recommended
            for solving with high precision (low values of `rtol` and `atol`).

            If not sure, first try to run 'RK45'. If it makes unusually many
            iterations, diverges, or fails, your problem is likely to be stiff and
            you should use 'Radau' or 'BDF'. 'LSODA' can also be a good universal
            choice, but it might be somewhat less convenient to work with as it
            wraps old Fortran code.

            You can also pass an arbitrary class derived from `OdeSolver` which
            implements the solver.
        t_eval : array_like or None, optional
            Times at which to store the computed solution, must be sorted and lie
            within `t_span`. If None (default), use points selected by the solver.
        dense_output : bool, optional
            Whether to compute a continuous solution. Default is False.
        events : callable, or list of callables, optional
            Events to track. If None (default), no events will be tracked.
            Each event occurs at the zeros of a continuous function of time and
            state. Each function must have the signature ``event(t, y)`` and return
            a float. The solver will find an accurate value of `t` at which
            ``event(t, y(t)) = 0`` using a root-finding algorithm. By default, all
            zeros will be found. The solver looks for a sign change over each step,
            so if multiple zero crossings occur within one step, events may be
            missed. Additionally each `event` function might have the following
            attributes:

                terminal: bool, optional
                    Whether to terminate integration if this event occurs.
                    Implicitly False if not assigned.
                direction: float, optional
                    Direction of a zero crossing. If `direction` is positive,
                    `event` will only trigger when going from negative to positive,
                    and vice versa if `direction` is negative. If 0, then either
                    direction will trigger event. Implicitly 0 if not assigned.

            You can assign attributes like ``event.terminal = True`` to any
            function in Python.
        vectorized : bool, optional
            Whether `fun` is implemented in a vectorized fashion. Default is False.
        args : tuple, optional
            Additional arguments to pass to the user-defined functions.  If given,
            the additional arguments are passed to all user-defined functions.
            So if, for example, `fun` has the signature ``fun(t, y, a, b, c)``,
            then `jac` (if given) and any event functions must have the same
            signature, and `args` must be a tuple of length 3.
        options
            Options passed to a chosen solver. All options available for already
            implemented solvers are listed below.
        first_step : float or None, optional
            Initial step size. Default is `None` which means that the algorithm
            should choose.
        max_step : float, optional
            Maximum allowed step size. Default is np.inf, i.e., the step size is not
            bounded and determined solely by the solver.
        rtol, atol : float or array_like, optional
            Relative and absolute tolerances. The solver keeps the local error
            estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
            relative accuracy (number of correct digits). But if a component of `y`
            is approximately below `atol`, the error only needs to fall within
            the same `atol` threshold, and the number of correct digits is not
            guaranteed. If components of y have different scales, it might be
            beneficial to set different `atol` values for different components by
            passing array_like with shape (n,) for `atol`. Default values are
            1e-3 for `rtol` and 1e-6 for `atol`.
        jac : array_like, sparse_matrix, callable or None, optional
            Jacobian matrix of the right-hand side of the system with respect
            to y, required by the 'Radau', 'BDF' and 'LSODA' method. The
            Jacobian matrix has shape (n, n) and its element (i, j) is equal to
            ``d f_i / d y_j``.  There are three ways to define the Jacobian:

                * If array_like or sparse_matrix, the Jacobian is assumed to
                  be constant. Not supported by 'LSODA'.
                * If callable, the Jacobian is assumed to depend on both
                  t and y; it will be called as ``jac(t, y)``, as necessary.
                  For 'Radau' and 'BDF' methods, the return value might be a
                  sparse matrix.
                * If None (default), the Jacobian will be approximated by
                  finite differences.

            It is generally recommended to provide the Jacobian rather than
            relying on a finite-difference approximation.
        jac_sparsity : array_like, sparse matrix or None, optional
            Defines a sparsity structure of the Jacobian matrix for a finite-
            difference approximation. Its shape must be (n, n). This argument
            is ignored if `jac` is not `None`. If the Jacobian has only few
            non-zero elements in *each* row, providing the sparsity structure
            will greatly speed up the computations [10]_. A zero entry means that
            a corresponding element in the Jacobian is always zero. If None
            (default), the Jacobian is assumed to be dense.
            Not supported by 'LSODA', see `lband` and `uband` instead.
        lband, uband : int or None, optional
            Parameters defining the bandwidth of the Jacobian for the 'LSODA'
            method, i.e., ``jac[i, j] != 0 only for i - lband <= j <= i + uband``.
            Default is None. Setting these requires your jac routine to return the
            Jacobian in the packed format: the returned array must have ``n``
            columns and ``uband + lband + 1`` rows in which Jacobian diagonals are
            written. Specifically ``jac_packed[uband + i - j , j] = jac[i, j]``.
            The same format is used in `scipy.linalg.solve_banded` (check for an
            illustration).  These parameters can be also used with ``jac=None`` to
            reduce the number of Jacobian elements estimated by finite differences.
        min_step : float, optional
            The minimum allowed step size for 'LSODA' method.
            By default `min_step` is zero.

        Returns
        -------
        Bunch object with the following fields defined:
        t : ndarray, shape (n_points,)
            Time points.
        y : ndarray, shape (n, n_points)
            Values of the solution at `t`.
        sol : `OdeSolution` or None
            Found solution as `OdeSolution` instance; None if `dense_output` was
            set to False.
        t_events : list of ndarray or None
            Contains for each event type a list of arrays at which an event of
            that type event was detected. None if `events` was None.
        y_events : list of ndarray or None
            For each value of `t_events`, the corresponding value of the solution.
            None if `events` was None.
        nfev : int
            Number of evaluations of the right-hand side.
        njev : int
            Number of evaluations of the Jacobian.
        nlu : int
            Number of LU decompositions.
        status : int
            Reason for algorithm termination:

                * -1: Integration step failed.
                *  0: The solver successfully reached the end of `tspan`.
                *  1: A termination event occurred.

        message : string
            Human-readable description of the termination reason.
        success : bool
            True if the solver reached the interval end or a termination event
            occurred (``status >= 0``).

        References
        ----------
        .. [1] J. R. Dormand, P. J. Prince, "A family of embedded Runge-Kutta
               formulae", Journal of Computational and Applied Mathematics, Vol. 6,
               No. 1, pp. 19-26, 1980.
        .. [2] L. W. Shampine, "Some Practical Runge-Kutta Formulas", Mathematics
               of Computation,, Vol. 46, No. 173, pp. 135-150, 1986.
        .. [3] P. Bogacki, L.F. Shampine, "A 3(2) Pair of Runge-Kutta Formulas",
               Appl. Math. Lett. Vol. 2, No. 4. pp. 321-325, 1989.
        .. [4] E. Hairer, G. Wanner, "Solving Ordinary Differential Equations II:
               Stiff and Differential-Algebraic Problems", Sec. IV.8.
        .. [5] `Backward Differentiation Formula
                <https://en.wikipedia.org/wiki/Backward_differentiation_formula>`_
                on Wikipedia.
        .. [6] L. F. Shampine, M. W. Reichelt, "THE MATLAB ODE SUITE", SIAM J. SCI.
               COMPUTE., Vol. 18, No. 1, pp. 1-22, January 1997.
        .. [7] A. C. Hindmarsh, "ODEPACK, A Systematized Collection of ODE
               Solvers," IMACS Transactions on Scientific Computation, Vol 1.,
               pp. 55-64, 1983.
        .. [8] L. Petzold, "Automatic selection of methods for solving stiff and
               nonstiff systems of ordinary differential equations", SIAM Journal
               on Scientific and Statistical Computing, Vol. 4, No. 1, pp. 136-148,
               1983.
        .. [9] `Stiff equation <https://en.wikipedia.org/wiki/Stiff_equation>`_ on
               Wikipedia.
        .. [10] A. Curtis, M. J. D. Powell, and J. Reid, "On the estimation of
                sparse Jacobian matrices", Journal of the Institute of Mathematics
                and its Applications, 13, pp. 117-120, 1974.
        .. [11] `Cauchy-Riemann equations
                 <https://en.wikipedia.org/wiki/Cauchy-Riemann_equations>`_ on
                 Wikipedia.
        .. [12] `Lotka-Volterra equations
                <https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations>`_
                on Wikipedia.
        .. [13] E. Hairer, S. P. Norsett G. Wanner, "Solving Ordinary Differential
                Equations I: Nonstiff Problems", Sec. II.
        .. [14] `Page with original Fortran code of DOP853
                <http://www.unige.ch/~hairer/software.html>`_.

        Examples
        --------
        Basic exponential decay showing automatically chosen time points.

        >>> from scipy.integrate import solve_ivp
        >>> def exponential_decay(t, y): return -0.5 * y
        >>> sol = solve_ivp(exponential_decay, [0, 10], [2, 4, 8])
        >>> print(sol.t)
        [ 0.          0.11487653  1.26364188  3.06061781  4.81611105  6.57445806
          8.33328988 10.        ]
        >>> print(sol.y)
        [[2.         1.88836035 1.06327177 0.43319312 0.18017253 0.07483045
          0.03107158 0.01350781]
         [4.         3.7767207  2.12654355 0.86638624 0.36034507 0.14966091
          0.06214316 0.02701561]
         [8.         7.5534414  4.25308709 1.73277247 0.72069014 0.29932181
          0.12428631 0.05403123]]

        Specifying points where the solution is desired.

        >>> sol = solve_ivp(exponential_decay, [0, 10], [2, 4, 8],
        ...                 t_eval=[0, 1, 2, 4, 10])
        >>> print(sol.t)
        [ 0  1  2  4 10]
        >>> print(sol.y)
        [[2.         1.21305369 0.73534021 0.27066736 0.01350938]
         [4.         2.42610739 1.47068043 0.54133472 0.02701876]
         [8.         4.85221478 2.94136085 1.08266944 0.05403753]]

        Cannon fired upward with terminal event upon impact. The ``terminal`` and
        ``direction`` fields of an event are applied by monkey patching a function.
        Here ``y[0]`` is position and ``y[1]`` is velocity. The projectile starts
        at position 0 with velocity +10. Note that the integration never reaches
        t=100 because the event is terminal.

        >>> def upward_cannon(t, y): return [y[1], -0.5]
        >>> def hit_ground(t, y): return y[0]
        >>> hit_ground.terminal = True
        >>> hit_ground.direction = -1
        >>> sol = solve_ivp(upward_cannon, [0, 100], [0, 10], events=hit_ground)
        >>> print(sol.t_events)
        [array([40.])]
        >>> print(sol.t)
        [0.00000000e+00 9.99900010e-05 1.09989001e-03 1.10988901e-02
         1.11088891e-01 1.11098890e+00 1.11099890e+01 4.00000000e+01]

        Use `dense_output` and `events` to find position, which is 100, at the apex
        of the cannonball's trajectory. Apex is not defined as terminal, so both
        apex and hit_ground are found. There is no information at t=20, so the sol
        attribute is used to evaluate the solution. The sol attribute is returned
        by setting ``dense_output=True``. Alternatively, the `y_events` attribute
        can be used to access the solution at the time of the event.

        >>> def apex(t, y): return y[1]
        >>> sol = solve_ivp(upward_cannon, [0, 100], [0, 10],
        ...                 events=(hit_ground, apex), dense_output=True)
        >>> print(sol.t_events)
        [array([40.]), array([20.])]
        >>> print(sol.t)
        [0.00000000e+00 9.99900010e-05 1.09989001e-03 1.10988901e-02
         1.11088891e-01 1.11098890e+00 1.11099890e+01 4.00000000e+01]
        >>> print(sol.sol(sol.t_events[1][0]))
        [100.   0.]
        >>> print(sol.y_events)
        [array([[-5.68434189e-14, -1.00000000e+01]]), array([[1.00000000e+02, 1.77635684e-15]])]

        As an example of a system with additional parameters, we'll implement
        the Lotka-Volterra equations [12]_.

        >>> def lotkavolterra(t, z, a, b, c, d):
        ...     x, y = z
        ...     return [a*x - b*x*y, -c*y + d*x*y]
        ...

        We pass in the parameter values a=1.5, b=1, c=3 and d=1 with the `args`
        argument.

        >>> sol = solve_ivp(lotkavolterra, [0, 15], [10, 5], args=(1.5, 1, 3, 1),
        ...                 dense_output=True)

        Compute a dense solution and plot it.

        >>> t = np.linspace(0, 15, 300)
        >>> z = sol.sol(t)
        >>> import matplotlib.pyplot as plt
        >>> plt.plot(t, z.T)
        >>> plt.xlabel('t')
        >>> plt.legend(['x', 'y'], shadow=True)
        >>> plt.title('Lotka-Volterra System')
        >>> plt.show()

        """
        if method not in METHODS and not (
                inspect.isclass(method) and issubclass(method, OdeSolver)):
            raise ValueError("`method` must be one of {} or OdeSolver class."
                             .format(METHODS))

        t0, tf = float(t_span[0]), float(t_span[1])

        if args is not None:
            # Wrap the user's fun (and jac, if given) in lambdas to hide the
            # additional parameters.  Pass in the original fun as a keyword
            # argument to keep it in the scope of the lambda.
            fun = lambda t, x, fun=fun: fun(t, x, *args)
            jac = options.get('jac')
            if callable(jac):
                options['jac'] = lambda t, x: jac(t, x, *args)

        if t_eval is not None:
            t_eval = np.asarray(t_eval)
            if t_eval.ndim != 1:
                raise ValueError("`t_eval` must be 1-dimensional.")

            if np.any(t_eval < min(t0, tf)) or np.any(t_eval > max(t0, tf)):
                raise ValueError("Values in `t_eval` are not within `t_span`.")

            d = np.diff(t_eval)
            if tf > t0 and np.any(d <= 0) or tf < t0 and np.any(d >= 0):
                raise ValueError("Values in `t_eval` are not properly sorted.")

            if tf > t0:
                t_eval_i = 0
            else:
                # Make order of t_eval decreasing to use np.searchsorted.
                t_eval = t_eval[::-1]
                # This will be an upper bound for slices.
                t_eval_i = t_eval.shape[0]

        if method in METHODS:
            method = METHODS[method]

        solver = method(fun, t0, y0, tf, vectorized=vectorized, **options)

        if t_eval is None:
            ts = [t0]
            ys = [y0]
        elif t_eval is not None and dense_output:
            ts = []
            ti = [t0]
            ys = []
        else:
            ts = []
            ys = []

        interpolants = []

        events, is_terminal, event_dir = prepare_events(events)

        if events is not None:
            if args is not None:
                # Wrap user functions in lambdas to hide the additional parameters.
                # The original event function is passed as a keyword argument to the
                # lambda to keep the original function in scope (i.e., avoid the
                # late binding closure "gotcha").
                events = [lambda t, x, event=event: event(t, x, *args)
                          for event in events]
            g = [event(t0, y0) for event in events]
            t_events = [[] for _ in range(len(events))]
            y_events = [[] for _ in range(len(events))]
        else:
            t_events = None
            y_events = None

        status = None
        while status is None:
            message = solver.step()

            if solver.status == 'finished':
                status = 0
            elif solver.status == 'failed':
                status = -1
                break

            t_old = solver.t_old
            t = solver.t
            y = solver.y

            if dense_output:
                sol = solver.dense_output()
                interpolants.append(sol)
            else:
                sol = None

            if events is not None:
                g_new = [event(t, y) for event in events]
                active_events = find_active_events(g, g_new, event_dir)
                if active_events.size > 0:
                    if sol is None:
                        sol = solver.dense_output()

                    root_indices, roots, terminate = handle_events(
                        sol, events, active_events, is_terminal, t_old, t)

                    for e, te in zip(root_indices, roots):
                        t_events[e].append(te)
                        y_events[e].append(sol(te))

                    if terminate:
                        status = 1
                        t = roots[-1]
                        y = sol(t)

                g = g_new

            if t_eval is None:
                ts.append(t)
                ys.append(y)
            else:
                # The value in t_eval equal to t will be included.
                if solver.direction > 0:
                    t_eval_i_new = np.searchsorted(t_eval, t, side='right')
                    t_eval_step = t_eval[t_eval_i:t_eval_i_new]
                else:
                    t_eval_i_new = np.searchsorted(t_eval, t, side='left')
                    # It has to be done with two slice operations, because
                    # you can't slice to 0th element inclusive using backward
                    # slicing.
                    t_eval_step = t_eval[t_eval_i_new:t_eval_i][::-1]

                if t_eval_step.size > 0:
                    if sol is None:
                        sol = solver.dense_output()
                    ts.append(t_eval_step)
                    ys.append(sol(t_eval_step))
                    t_eval_i = t_eval_i_new

            if t_eval is not None and dense_output:
                ti.append(t)

        message = MESSAGES.get(status, message)

        if t_events is not None:
            t_events = [np.asarray(te) for te in t_events]
            y_events = [np.asarray(ye) for ye in y_events]

        if t_eval is None:
            ts = np.array(ts)
            ys = np.vstack(ys).T
        else:
            ts = np.hstack(ts)
            ys = np.hstack(ys)

        if dense_output:
            if t_eval is None:
                sol = OdeSolution(ts, interpolants)
            else:
                sol = OdeSolution(ti, interpolants)
        else:
            sol = None

        return OdeResult(t=ts, y=ys, sol=sol, t_events=t_events, y_events=y_events,
                         nfev=solver.nfev, njev=solver.njev, nlu=solver.nlu,
                         status=status, message=message, success=status >= 0)
    File:      /anaconda3/lib/python3.8/site-packages/scipy/integrate/_ivp/ivp.py
    Type:      function

### Pathline tracing

Pathlines are computed through numerical integration of the velocity vector. Pathlines are computed with the `timtrace` function. The `timtrace` function takes as input arguments the starting locations of the pathline (`xstart`, `ystart`, `zstart`), a list with the starting time and end time of the pathline (`tstartend`), a time offset (`tstartoffset`), and a maximum time step (`tstep`). The pathline starts at the starting time plus the time offset (`tstartoffset`). When the starting time is at the time of a change in boundary condition (e.g., a well starts pumping), the time offset can not be smaller than `tmin`. The `timtrace` function has several keyword arguments to affect the numerical integration procedure. The `hstepmax` keyword is used here, which is the maximum horizontal step (in the length units consistently used throughout the model).

The `timtrace` function returns a dictionary with three three entries:

- `'xyzt'` : 2D array with four columns: x, y, z, t along pathline
- `'message'` : list with text messages of each section of the pathline
- `'status'` : numerical indication of the result. Negative is likely undesirable.

The status can be one of the following:
- -2 : reached maximum number of steps before reaching maximum time
- -1 : starting z value not inside aquifer
- +1 : reached maximum time
- +2 : reached element

The `message` is automatically printed to the screen unless the keyword `silent` is set to `True`.

### Example 1
Consider a well that starts pumping at $t=0$ in a confined aquifer. The well is located at the origin of the coordinate system.

```python
# parameters
Q = 100 # discharge of well, m^3/d
k = 10 # hydraulic conductivity, m/d
H = 10 # thickness of aquifer, m
Ss = 1e-4 # specific storage, m^(-1)
npor = 0.3 # porosity, -
xw = 0 # x-location of well
yw = 0 # y-location of well
rw = 0.3 # radius of well
```

```python
ml = tt.ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], tmin=0.001, tmax=1000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, Q)], rw=rw)
ml.solve()
```

    self.neq  1
    solution complete

A pathline is started at $(x,y,z)=(10, 10, 5)$ and followed for 10 days.

```python
trace = tt.timtrace(ml, xstart=10, ystart=10, zstart=0.5 * H, tstartend=[0, 10],
                    tstartoffset=0.001, tstep=1, hstepmax=2)
```

    reached maximum time tmax

The output of the `timtrace` function is stored in the `trace` dictionary. The entries of the dictionary are printed below. Note that the pathline starts at $t=0.001$ d, which is the specified `tstartoffset`, that steps are taken with a length of 1 day, and that the pathline is terminated when the maximum time of 10 days is reached.

```python
print('xyzt array of pathline:')
print(trace['xyzt'])
print('trace message:', trace['message'])
print('trace status:', trace['status'])
```

    xyzt array of pathline:
    [[1.00000000e+01 1.00000000e+01 5.00000000e+00 0.00000000e+00]
     [1.00000000e+01 1.00000000e+01 5.00000000e+00 1.00000000e-03]
     [9.78472212e+00 9.78472212e+00 5.00000000e+00 1.00100000e+00]
     [9.50986380e+00 9.50986380e+00 5.00000000e+00 2.00100000e+00]
     [9.22677289e+00 9.22677289e+00 5.00000000e+00 3.00100000e+00]
     [8.93469839e+00 8.93469839e+00 5.00000000e+00 4.00100000e+00]
     [8.63273762e+00 8.63273762e+00 5.00000000e+00 5.00100000e+00]
     [8.31981832e+00 8.31981832e+00 5.00000000e+00 6.00100000e+00]
     [7.99465612e+00 7.99465612e+00 5.00000000e+00 7.00100000e+00]
     [7.65569276e+00 7.65569276e+00 5.00000000e+00 8.00100000e+00]
     [7.30100747e+00 7.30100747e+00 5.00000000e+00 9.00100000e+00]
     [6.93806421e+00 6.93806421e+00 5.00000000e+00 1.00000000e+01]]
    trace message: ['reached maximum time tmax']
    trace status: [1]

When the pathline is computed for a max of 20 days (i.e., the last entry in `tstartend` equals 20), the well is reached before $t=20$ d is reached

```python
trace = tt.timtrace(ml, xstart=10, ystart=10, zstart=0.5 * H, tstartend=[0, 20],
                    tstartoffset=0.001, tstep=1, hstepmax=2)
```

    reached element of type well: Well at (0.0, 0.0)

```python
print('last two entries in xyzt:')
print(trace['xyzt'][-2:])
```

    last two entries in xyzt:
    [[ 0.94871432  0.94871432  5.         18.8683065 ]
     [ 0.          0.          5.         19.20762075]]

When the horizontal stepsize is larger than the specified `hstepmax` value, the timestep is reduced. For example, specifying `hstepmax=0.5` and following the pathline for 4 days gives

```python
trace = tt.timtrace(ml, xstart=10, ystart=10, zstart=0.5 * H, tstartend=[0, 1],
                    tstartoffset=0.001, tstep=1, hstepmax=0.1)
```

    reached maximum time tmax

```python
xyzt = trace['xyzt']
print('xyzt:')
print(trace['xyzt'])
x0, y0, z0, t0 = xyzt[1]
x1, y1, z1, t1 = xyzt[2]
print(f'length of first step: {np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2):.2f}')
```

    xyzt:
    [[1.00000000e+01 1.00000000e+01 5.00000000e+00 0.00000000e+00]
     [1.00000000e+01 1.00000000e+01 5.00000000e+00 1.00000000e-03]
     [9.92928932e+00 9.92928932e+00 5.00000000e+00 3.31474847e-01]
     [9.85857864e+00 9.85857864e+00 5.00000000e+00 5.95520257e-01]
     [9.78786797e+00 9.78786797e+00 5.00000000e+00 8.57557196e-01]
     [9.74928649e+00 9.74928649e+00 5.00000000e+00 1.00000000e+00]]
    length of first step: 0.10

The other keyword arguments are `nstepmax=100`, `silent=False`, and `correctionstep=True`. The `nstepmax` is the maximum number of steps. Numerical integration stops when the maximum number of steps is reached and the returned `status` is `-2`. For most practical cases, this is an undesirable result and the maximum number of steps should be increased. Setting `silent=True` prevents the message from being printed to the screen, which may be useful when a lot of pathlines are computed, for example in a loop. When the `correctionstep` is set to `True` (default), the numerical integration scheme is a predictor-correcter scheme. When `correctionstep` is set to `False`, the integration scheme is forward integration through time, which is less accurate but quicker.

### Example 2
Consider a well in a confined aquifer. The well starts pumping with discharge $Q$ at time $t=0$ and starts injection with discharge $Q$ at time $t=10$ d. The well is located at the origin of the coordinate system.

```python
ml = tt.ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], tmin=0.001, tmax=1000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, Q), (10, -Q)], rw=rw)
ml.solve()
```

    self.neq  1
    solution complete

A pathline is started at $(x,y,z)=(10, 10, 5)$ and followed for 20 days. The `tstartend` list is now `[0, 10, 20]` and `tstartoffset=0.001`. This means that the pathline consists of two sections. The first section starts at $t=0.001$ d and is followed up till $t=10$ d. The second section starts  at $t=10.001$ d (from the endpoint at $t=10$ d of the first section) and is followed up till $t=20$ d. The `nstepmax` keyword is the maximum number of steps for each section of the pathline. There are two messages printed to the screen, one for each section.

```python
trace = tt.timtrace(ml, xstart=10, ystart=10, zstart=0.5 * H, tstartend=[0, 10, 20],
                    tstartoffset=0.001, tstep=1, hstepmax=2)
```

    reached maximum time tmax
    reached maximum time tmax

The $x,y,z,t$ array is printed below. Note that the first section ends at $t=10$ d.
After 20 d, the pathline ends at (almost) the same location as it started. The small discrepancy is caused by numerical error of the integration scheme and by the increase in storage that is neglected.

```python
xyzt = trace['xyzt']
print('xyzt:')
print(trace['xyzt'])
```

    xyzt:
    [[1.00000000e+01 1.00000000e+01 5.00000000e+00 0.00000000e+00]
     [1.00000000e+01 1.00000000e+01 5.00000000e+00 1.00000000e-03]
     [9.78472212e+00 9.78472212e+00 5.00000000e+00 1.00100000e+00]
     [9.50986380e+00 9.50986380e+00 5.00000000e+00 2.00100000e+00]
     [9.22677289e+00 9.22677289e+00 5.00000000e+00 3.00100000e+00]
     [8.93469839e+00 8.93469839e+00 5.00000000e+00 4.00100000e+00]
     [8.63273762e+00 8.63273762e+00 5.00000000e+00 5.00100000e+00]
     [8.31981832e+00 8.31981832e+00 5.00000000e+00 6.00100000e+00]
     [7.99465612e+00 7.99465612e+00 5.00000000e+00 7.00100000e+00]
     [7.65569276e+00 7.65569276e+00 5.00000000e+00 8.00100000e+00]
     [7.30100747e+00 7.30100747e+00 5.00000000e+00 9.00100000e+00]
     [6.93806421e+00 6.93806421e+00 5.00000000e+00 1.00000000e+01]
     [6.93806421e+00 6.93806421e+00 5.00000000e+00 1.00010000e+01]
     [7.23288902e+00 7.23288902e+00 5.00000000e+00 1.10010000e+01]
     [7.59064546e+00 7.59064546e+00 5.00000000e+00 1.20010000e+01]
     [7.93233482e+00 7.93233482e+00 5.00000000e+00 1.30010000e+01]
     [8.25991940e+00 8.25991940e+00 5.00000000e+00 1.40010000e+01]
     [8.57500768e+00 8.57500768e+00 5.00000000e+00 1.50010000e+01]
     [8.87892681e+00 8.87892681e+00 5.00000000e+00 1.60010000e+01]
     [9.17278541e+00 9.17278541e+00 5.00000000e+00 1.70010000e+01]
     [9.45752040e+00 9.45752040e+00 5.00000000e+00 1.80010000e+01]
     [9.73393184e+00 9.73393184e+00 5.00000000e+00 1.90010000e+01]
     [1.00061463e+01 1.00061463e+01 5.00000000e+00 2.00000000e+01]]

### Example 3
Consider an injection well in a semi-confined aquifer. The well starts injecting with discharge $Q$ at time $t=0$ and starts injection with discharge $Q$ at time $t=10$ d. The well is located at the origin of the coordinate system.

```python
# parameters
k = 20 # hydraulic conductivity aquifer, m/d
H = 10 # thickness of aquifers, m
Hstar = 2 # thickness of leaky layer, m
c = 100 # resistance of leaky layer, d
Ss = 1e-4 # specific storage of both aquifers, m^(-1)
npor = 0.3 # porosity of both aquifers, -
Q = 1000 # discharge of well in aquifer 1, m^3/d
xw = 0 # x-location of well
yw = 0 # y-location of well
rw = 0.3 # radius of well
```

```python
ml = tt.ModelMaq(kaq=[k], z=[H + Hstar, H, 0], c=[c], Saq=Ss,
                 topboundary='semi', tmin=0.001, tmax=1000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, -Q)], layers=0, rw=0.3)
ml.solve()
```

    self.neq  1
    solution complete

Nine pathlines are started at the well screen at different elevations in the aquifer and followed for a max of 1000 days. Five of the nine pathlines end at the top of the semi-confining layer within the 1000 days (the five pathlines that start at the highest elevations).

```python
plt.subplot(111, aspect=10)
zstart = np.arange(1.0, 10, 1)
for zs in zstart:
    trace = tt.timtrace(ml, xstart=rw, ystart=0, zstart=zs,
                        tstartend=[0, 1000], tstartoffset=0.01, tstep=100,
                        nstepmax=100, hstepmax=2, silent=True)
    xyzt = trace['xyzt']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
for y in [0, H, H + Hstar]:
    plt.axhline(y, color='k')
```

```python
ml.head(5, 5, 5, layers=0).shape
```

    (1, 1)

```python
x = np.linspace(rw, 200, 100)
h = ml.headalongline(x, 0, 0.1)
plt.plot(x, h[0, 0])
h = ml.headalongline(x, 0, 1)
plt.plot(x, h[0, 0])
```

```python
t = np.logspace(-3, 2, 100)
h = w.headinside(t)
plt.semilogx(t, h[0])
```

```python
np.sqrt(k * H * c)
```

    141.4213562373095

```python
# parameters
k0 = 20 # hydraulic conductivity aquifer 0, m/d
k1 = 40 # hydraulic conductivity aquifer 1, m/d
H = 10 # thickness of both aquifers, m
Hstar = 2 # thickness of leaky layer, m
c = 100 # resistance of leaky layer, d
Ss = 1e-4 # specific storage of both aquifers, m^(-1)
npor = 0.3 # porosity of both aquifers, -
Q = 100 # discharge of well in aquifer 1, m^3/d
xw = 0 # x-location of well
yw = 0 # y-location of well
rw = 0.3 # radius of well
```

```python
ml = tt.ModelMaq(kaq=[k0, k1], z=[H + Hstar + H, H + Hstar, H, 0], c=[c], Saq=[Ss, Ss],
                 tmin=0.01, tmax=10000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, Q)], layers=1, rw=0.3)
ml.solve()
```

    self.neq  1
    solution complete

```python
zstart = np.hstack((np.arange(1, 10, 2), np.arange(13, 22, 2)))
for zs in zstart:
    trace = tt.timtrace(ml, xstart=100, ystart=0, zstart=zs,
                        tstartend=[0, 10000], tstartoffset=0.01, tstep=100,
                        nstepmax=100, hstepmax=2, silent=True)
    xyzt = trace['xyzt']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
for y in [0, H, H + Hstar, H + Hstar + H]:
    plt.axhline(y, color='k')
plt.axis('scaled');
```

```python
trace = tt.timtrace(ml, xstart=100, ystart=0, zstart=1,
                        tstartend=[0, 10000], tstartoffset=0.01, tstep=100,
                        nstepmax=100, hstepmax=2, silent=True)
trace['xyzt']
```

    array([[1.00000000e+02, 0.00000000e+00, 1.00000000e+00, 0.00000000e+00],
           [1.00000000e+02, 0.00000000e+00, 1.00000000e+00, 1.00000000e-02],
           [9.80000000e+01, 0.00000000e+00, 9.97471846e-01, 5.32099582e+01],
           [9.60000000e+01, 0.00000000e+00, 9.94584121e-01, 9.42742075e+01],
           [9.40000000e+01, 0.00000000e+00, 9.91698424e-01, 1.34373621e+02],
           [9.20000000e+01, 0.00000000e+00, 9.88815329e-01, 1.73514044e+02],
           [9.00000000e+01, 0.00000000e+00, 9.85935620e-01, 2.11700770e+02],
           [8.80000000e+01, 0.00000000e+00, 9.83060151e-01, 2.48938970e+02],
           [8.60000000e+01, 0.00000000e+00, 9.80189832e-01, 2.85233771e+02],
           [8.40000000e+01, 0.00000000e+00, 9.77325613e-01, 3.20590297e+02],
           [8.20000000e+01, 0.00000000e+00, 9.74468487e-01, 3.55013678e+02],
           [8.00000000e+01, 0.00000000e+00, 9.71619485e-01, 3.88509052e+02],
           [7.80000000e+01, 0.00000000e+00, 9.68779680e-01, 4.21081569e+02],
           [7.60000000e+01, 0.00000000e+00, 9.65950189e-01, 4.52736388e+02],
           [7.40000000e+01, 0.00000000e+00, 9.63132175e-01, 4.83478676e+02],
           [7.20000000e+01, 0.00000000e+00, 9.60326845e-01, 5.13313607e+02],
           [7.00000000e+01, 0.00000000e+00, 9.57535460e-01, 5.42246361e+02],
           [6.80000000e+01, 0.00000000e+00, 9.54759334e-01, 5.70282119e+02],
           [6.60000000e+01, 0.00000000e+00, 9.51999837e-01, 5.97426062e+02],
           [6.40000000e+01, 0.00000000e+00, 9.49258399e-01, 6.23683367e+02],
           [6.20000000e+01, 0.00000000e+00, 9.46536518e-01, 6.49059206e+02],
           [6.00000000e+01, 0.00000000e+00, 9.43835760e-01, 6.73558741e+02],
           [5.80000000e+01, 0.00000000e+00, 9.41157765e-01, 6.97187121e+02],
           [5.60000000e+01, 0.00000000e+00, 9.38504256e-01, 7.19949476e+02],
           [5.40000000e+01, 0.00000000e+00, 9.35877042e-01, 7.41850918e+02],
           [5.20000000e+01, 0.00000000e+00, 9.33278027e-01, 7.62896533e+02],
           [5.00000000e+01, 0.00000000e+00, 9.30709220e-01, 7.83091378e+02],
           [4.80000000e+01, 0.00000000e+00, 9.28172738e-01, 8.02440476e+02],
           [4.60000000e+01, 0.00000000e+00, 9.25670825e-01, 8.20948811e+02],
           [4.40000000e+01, 0.00000000e+00, 9.23205858e-01, 8.38621321e+02],
           [4.20000000e+01, 0.00000000e+00, 9.20780362e-01, 8.55462896e+02],
           [4.00000000e+01, 0.00000000e+00, 9.18397028e-01, 8.71478367e+02],
           [3.80000000e+01, 0.00000000e+00, 9.16058728e-01, 8.86672503e+02],
           [3.60000000e+01, 0.00000000e+00, 9.13768539e-01, 9.01050000e+02],
           [3.40000000e+01, 0.00000000e+00, 9.11529764e-01, 9.14615475e+02],
           [3.20000000e+01, 0.00000000e+00, 9.09345967e-01, 9.27373454e+02],
           [3.00000000e+01, 0.00000000e+00, 9.07221003e-01, 9.39328366e+02],
           [2.80000000e+01, 0.00000000e+00, 9.05159062e-01, 9.50484522e+02],
           [2.60000000e+01, 0.00000000e+00, 9.03164719e-01, 9.60846111e+02],
           [2.40000000e+01, 0.00000000e+00, 9.01243000e-01, 9.70417173e+02],
           [2.20000000e+01, 0.00000000e+00, 8.99399458e-01, 9.79201582e+02],
           [2.00000000e+01, 0.00000000e+00, 8.97640272e-01, 9.87203018e+02],
           [1.80000000e+01, 0.00000000e+00, 8.95972386e-01, 9.94424927e+02],
           [1.60000000e+01, 0.00000000e+00, 8.94403718e-01, 1.00087048e+03],
           [1.40000000e+01, 0.00000000e+00, 8.92943317e-01, 1.00654246e+03],
           [1.20000000e+01, 0.00000000e+00, 8.91601731e-01, 1.01144317e+03],
           [1.00000000e+01, 0.00000000e+00, 8.90391622e-01, 1.01557417e+03],
           [8.00000001e+00, 0.00000000e+00, 8.89328625e-01, 1.01893583e+03],
           [6.00000001e+00, 0.00000000e+00, 8.88433035e-01, 1.02152620e+03],
           [4.00000001e+00, 0.00000000e+00, 8.87733671e-01, 1.02333778e+03],
           [2.00000001e+00, 0.00000000e+00, 8.87280790e-01, 1.02434351e+03],
           [1.00000079e-08, 0.00000000e+00, 8.86372147e-01, 1.02585182e+03],
           [0.00000000e+00, 0.00000000e+00, 8.86360044e-01, 1.02586878e+03]])

```python
k0 = 10
k1 = 40
H = 10
Hstar = 2
c = 100
T = k * H
Ss = 1e-4
S = Ss * H
```

```python
zstart = np.linspace(10.5, 14.5, 10)
for zs in zstart:
    trace = tt.timtrace(ml, xstart=0.1, ystart=.1, zstart=zs,
                     tstartend=[0, 100], tstartoffset=0.01, tstep=5,
                     nstepmax=40, hstepmax=2, silent=True)
    xyzt = trace['xyzt']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
for y in [5, 10, 15]:
    plt.axhline(y, color='k')
plt.axis('scaled');
```

```python
ml = ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], tmin=0.01, tmax=1000, M=10)
w = Well(ml, xw=0, yw=0, tsandQ=[(0, -Q), (100, Q), (200, -Q)], rw=0.3)
ml.solve()
trace = timtrace(ml, xstart=0.3, ystart=0, zstart=0,
                 tstartend=[0, 100, 200, 400], tstartoffset=0.01, deltlist=10,
                 nstepmax=120, hstepmax=2, correctionstep=True)
xyzt = trace['trace']
```

    1.44

```python

```

    Signature:
    timtrace(
        ml,
        xstart,
        ystart,
        zstart,
        tstartend,
        tstartoffset,
        deltlist,
        nstepmax=100,
        hstepmax=10,
        silent=False,
        correctionstep=True,
    )
    Docstring:
    Compute a pathline by numerical integration of the velocity vector.
    Pathline is broken up in sections for which starting times are provided.
    Pathline is computed from first starting time + offset until second
    starting time, then continued from second starting time + offset until
    third starting time, etc.

    Parameters
    ----------
    model : Model object
        model
    xstart : float
        x-coordinate of starting location of pathline
    ystart : float
        y-coordinate of starting location of pathline
    zstart : float
        z-coordinate of starting location of pathline
    tstartend : list
        list of starting times of pathline. last entry is the ending time.
    tstartoffset : float
        time after starting time when pathline is started. this value can not
        be smaller than tmin.
    deltlist : scalar or list
        maximum time step for each step along pathline. either one value for
        all sections, or list with value for each section.
    nstepmax : integer
        maximu number of steps per section
    hstepmax : float or integer
        maximum length of horizontal step
    silent : boolean
        parameter to indicate if message should be printed to the screen for
        each section of the pathline
    correctionstep : boolean
        parameter to indicate if a correction step (Euler's method) should
        be taken. Taking a correction step is more accurate, especially for
        curved pathlines.

    Returns
    --------
    result : dictionary with three entries

        * xyzt : 2D array with four columns: x, y, z, t along pathline
        * message : list with text messages of each section of the pathline
        * status : numerical indication of the result. Negative is likely
        undesirable.

            * -2 : reached maximum number of steps before reaching maximum time
            * -1 : starting z value not inside aquifer
            * +1 : reached maximum time
            * +2 : reached element
    File:      ~/git/ttim/ttim/trace.py
    Type:      function

```python

```

Pathlines are computed through numerical integration of the velocity vector.
Pathline is broken up in sections for which starting times are provided.
Pathline is computed from first starting time + offset until second
starting time, then continued from second starting time + offset until
third starting time, etc.

```python
import numpy as np
import matplotlib.pyplot as plt
from ttim import *
```

Start well with `tsandQ=[(0, -Q), (100, Q), (200, -Q)]`

```python
Q = 100
npor = 0.3
k = 10
H = 10
T = k * H
Ss = 1e-4
S = Ss * H
ml = ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], tmin=0.01, tmax=1000, M=10)
w = Well(ml, xw=0, yw=0, tsandQ=[(0, -Q), (100, Q), (200, -Q)], rw=0.3)
ml.solve()
trace = timtrace(ml, xstart=0.3, ystart=0, zstart=0,
                 tstartend=[0, 100, 200, 400], tstartoffset=0.01, deltlist=10,
                 nstepmax=120, hstepmax=2, correctionstep=True)
xyzt = trace['trace']
```

    self.neq  1
    solution complete
    reached maximum time tmax
    reached maximum time tmax
    reached maximum time tmax

```python
plt.plot(xyzt[:, 3], xyzt[:, 0])
plt.grid()
```

```python
trace['status'], trace['message']
```

    ([1, 1, 1],
     ['reached maximum time tmax',
      'reached maximum time tmax',
      'reached maximum time tmax'])

#### Terminates at well

```python
ml = ModelMaq(kaq=[k], z=[H, 0], Saq=[Ss], tmin=0.01, tmax=1000, M=10)
w = Well(ml, xw=0, yw=0, tsandQ=[(0, -Q), (100, Q)], rw=0.3, label='testwell')
ml.solve()
trace = timtrace(ml, xstart=0.3, ystart=0, zstart=0,
                 tstartend=[0, 100, 300], tstartoffset=0.01, deltlist=10,
                 nstepmax=120, hstepmax=2, correctionstep=True)
xyzt = trace['trace']
print(trace['status'], trace['message'])
print(xyzt[-3:])
```

    self.neq  1
    solution complete
    reached maximum time tmax
    reached well element with label: testwell
    [1, 2] ['reached maximum time tmax', 'reached well element with label: testwell']
    [[  3.8943918    0.           0.         201.42194579]
     [  1.8943918    0.           0.         202.38286201]
     [  0.           0.           0.         203.05932345]]

#### Compare to numerical integration of velocity from Theis equation
\begin{equation}
Q_r = -T\frac{\partial h}{\partial r} = -T\frac{\text{d} h}{\text{d} u}\frac{\partial u}{\partial r} = -\frac{Q}{2\pi} \frac{1}{r}\exp(-u)
\end{equation}
\begin{equation}
u =  \frac{S r^2}{4T(t-t_0)}
\end{equation}

```python
def vxytheis(t, xy):
    Q = -100
    npor = 0.3
    k = 10
    H = 10
    Ss = 1e-4
    T = k * H
    S = Ss * H
    x, y = xy
    r = np.sqrt(x ** 2 + y ** 2)
    u = S * r ** 2 / (4 * T * t)
    Qr = -Q / (2 * np.pi) / r * np.exp(-u)
    vr = Qr / (H * npor)
    vx = vr * x / r
    vy = vr * y / r
    return np.array([vx, vy])
```

```python
from scipy.integrate import solve_ivp
t = np.linspace(0.01, 100, 100)
path0 = solve_ivp(vxytheis, (0.01, 100), y0=[0.3, 0], t_eval=t)
plt.plot(path0.t, path0.y[0]);
```

#### Velocities in multi-layer system

```python
Q = 100
npor = 0.3
k0 = 10
k1 = 40
H = 10
Hstar = 2
c = 100
T = k * H
Ss = 1e-4
S = Ss * H
ml = tt.ModelMaq(kaq=[k0, k1], z=[2 * H + Hstar, H + Hstar, H, 0], c=[c], Saq=[Ss], tmin=0.01, tmax=10000, M=10)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, -Q)], layers=0, rw=0.3)
ml.solve()
```

    self.neq  1
    solution complete

```python
zstart = np.linspace(10.5, 14.5, 10)
for zs in zstart:
    trace = tt.timtrace(ml, xstart=0.1, ystart=.1, zstart=zs,
                     tstartend=[0, 100], tstartoffset=0.01, tstep=5,
                     nstepmax=40, hstepmax=2, silent=True)
    xyzt = trace['xyzt']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
for y in [5, 10, 15]:
    plt.axhline(y, color='k')
plt.axis('scaled');
```

```python
for z in np.arange(13, 22):
    trace = tt.timtrace(ml, xstart=0.3, ystart=0, zstart=z,
                     tstartend=[0, 1000], tstartoffset=0.01, deltlist=10,
                     nstepmax=500, hstepmax=2, correctionstep=True, silent=True)
    xyzt = trace['trace']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
```

#### Model3D

```python
Q = 100
npor = 0.3
k = 10
H = 10
Hstar = 2
c = 1000
T = k * H
#Ss = 1e-4
Ss = 1e-8
S = Ss * H
ml = tt.Model3D(kaq=10, z=[20, 15, 10, 5, 0], tmin=0.01, tmax=1000)
w = tt.Well(ml, xw=0, yw=0, tsandQ=[(0, -Q), (100, Q)], layers=1, rw=0.1)
ml.solve()
```

    self.neq  1
    solution complete

```python
ml.head(1
```

```python
zstart = np.linspace(10.01, 14.99, 31)
for zs in zstart:
    trace = tt.timtrace(ml, xstart=0.1, ystart=.1, zstart=zs,
                     tstartend=[0, 100], tstartoffset=0.01, tstep=5,
                     nstepmax=40, hstepmax=2, silent=True)
    xyzt = trace['xyzt']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
for y in [5, 10, 15]:
    plt.axhline(y, color='k')
plt.axis('scaled');
```

```python
ml.head(0, 0, 100)
```

    Warning, some of the times are smaller than tmin after
    a change in boundary condition. nans are substituted

    array([[nan],
           [nan],
           [nan],
           [nan]])

```python
h = ml.headalongline(np.linspace(0, 20, 100), np.zeros(100), t=100 - 1e-6)
```

```python
w.headinside(99)
```

    array([[1.90565058]])

```python
zstart = np.linspace(10.01, 14.99, 31)
for zs in zstart:
    trace = timtrace(ml, xstart=0.1, ystart=0.1, zstart=zs,
                     tstartend=[0, 100, 200], tstartoffset=0.01, deltlist=5,
                     nstepmax=40, hstepmax=2, silent=True, correctionstep=True)
    xyzt = trace['trace']
    plt.plot(xyzt[:, 0], xyzt[:, 2])
for y in [5, 10, 15]:
    plt.axhline(y, color='k')
plt.axis('scaled');
```


```python
import sympy as sm
import sympy.vector
import sympy.plotting
sigma_s = sm.symbols('\hat{\sigma}^2')
lamb, x,y,a, b = sm.symbols('lambda x,y a b')
B = sm.vector.CoordSys3D('B')
#### transformation factor
# P_{B}^(N) = N.e [] = P[](N_e1 + N_e2)
P = sm.Matrix([[1,1],[4,1]])

I = sm.eye(2)
lI = lamb * I

V = sm.Matrix([a,b])

#### A*V = lamb*V
# A*V = lamb*I*V
# A*V - lamb*I*V = 0
# (A - lamb*I)*V = 0
# A 와 lamb*I, 두 벡터가 평행 해야만 빼서 0을 만들수 있으므로
# 평행한 벡터의 면적은 0 !!
# det(A - lamb*I) = 0
s = P - lamb*I
s = s.det()
s = sm.solve(s,lamb)
## substitute lamb to V
# (A - s[0] * I) *V = 0
s1 = (P - s[0]*I) * V
s2 = (P - s[1]*I) * V

s1 = V.subs(sm.solve(s1))
s2 = V.subs(sm.solve(s2))
v1 = sm.vector.matrix_to_vector(s1,B)
```

```python
f = sm.symbols('f',cls=sm.Function)
sm.dsolve(f(x).diff(x,2) - f(x),f(x))
```

$\displaystyle f{\left(x \right)} = C_{1} e^{- x} + C_{2} e^{x}$

# [basis](https://www.youtube.com/watch?v=PT8FyU0dd3k) > standard basis(orthogonal)
>> $ S , N \in \mathbb R^2\\
S = \{e_1,e_2 \}, e_1=(1,0),e_2=(0,1)\\
N =
\begin{bmatrix}1 & 1 \\ 1 & -1 \end{bmatrix}\\
[V]_S = \big[?\big]_S^N\:[v]_N \\
[V]_N = \big[?\big]_N^S\:[v]_S \\
\therefore P_{N}^{S} = \big(? \big)_{2\times 2}\\
N.e_1 = \begin{bmatrix} 1 \\ 0 \end{bmatrix}
\Leftarrow
\begin{bmatrix}? \\ ? \end{bmatrix}_N^S
\begin{bmatrix}1\\1\end{bmatrix}
+
\begin{bmatrix}? \\ ? \end{bmatrix}_N^S
\begin{bmatrix}1 \\ -1 \end{bmatrix}
\\
N.e_2 = \begin{bmatrix} 0 \\ 1 \end{bmatrix}
\Leftarrow
\begin{bmatrix}? \\ ? \end{bmatrix}_N^S \begin{bmatrix}1\\1\end{bmatrix}
+
\begin{bmatrix}? \\ ? \end{bmatrix}_N^S
\begin{bmatrix}1 \\ -1 \end{bmatrix}
\\
\therefore
P_N^S = \begin{bmatrix} \frac{1}{2} & \frac{1}{2}\\
\frac{1}{2} & -\frac{1}{2} \end{bmatrix} \\
\quad [v]_N  =  P_N^S\:[v]_S
$
> ## that is inverse
>> ## $ N^{-1}
=
\begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix}^{-1}
\iff
P_N^S = \begin{bmatrix} \frac{1}{2} & \frac{1}{2}\\
\frac{1}{2} & -\frac{1}{2} \end{bmatrix}
$

# origin
> 기시점, 원점
>> 참조 point
>> reference point: one point를 중심으로 모든 대상을 상대적인 비율(거리)로 나타내려할때 그 중심 point

# axis
> 축
>> 원점에 서있을때 정면 옆면 윗면방향으로 무한히 확장한 축

# perspective
> 관점.
>> 목적 대상(object)을 원점과의 상대적인 물리적인 관계로 상태(states)를 기술하는 방법

# Scaling Factor
> 축척

>> line
>>> 1D

>> Area
>>> Determinant = scaling factor for areas in matrix

>> Volumn
>>> corss product = scaling factor fo rarea in matix

```python
    # 직각좌표계
import sympy as sm
import sympy.vector

### nabla operator = Del()
###    sympy.vector.Del()
nabla = sm.vector.Del()

# 직각 좌표계
B = sm.vector.CoordSys3D('')

# 직각 좌표계의 base vector(i,j,k), base scalar(x,y,z)
B.base_scalars(), B.base_vectors()

# vector in i,j,k vector
v1 = 1*B.i+2*B.j+3*B.k
type(v1)

v2 = 3*B.i+4*B.j+5*B.k
type(v2)

# scalar in x,y,z not a vector
v0 = B.x + B.y + B.z
type(v0)

# to matrix of the Coordinate system,where, v is vector
v1.to_matrix(B)

v1.magnitude() == sm.sqrt(v1.dot(v1))
v1.cross(v2)
v1.normalize()

### projection vector ###
# v1.projection(v2)
# v1.dot(v2) = ||v1|| ||v2|| cos(theta)
# cos(theta) = (v1.dot(v2))/(||v1|| ||v2||)
# v1.project(v2) = ||v2|| cost(theta) \hat{v1} = (v1.dot(v2))/(|\v1||^2) x \vec{v1}
# v1.project(v2) = \frac{||v2||cost(theta)}{\hat{v1}} = \frac{(v1.dot(v2))}{|\v1||^2} \vec{v1}
# v1.project(v2) = \frac{(v1.dot(v2))}{v1.dot(v1)}\vec{v1}
v1.projection(v2) == v1.dot(v2)/v1.dot(v1)*v1

#### nabla Del(), where in field potential energy ###
# \nabla \cdot = divergence
# \nabla \times = curl
# \nabla * f(x,y,z) = gradient, where f(x,y,z) is scalar function(that return real numbers)
u_field = 2 * B.x * B.y**2 * B.i + 3 * B.z**2 * B.y*B.j + 4 * B.x**3 * B.z * B.k
type(u_field)

f_field = 3* B.x**2 + B.y**3 + B.z**2
type(f_field)

nabla = sm.vector.Del()
nabla.dot(u_field)
nabla.dot(u_field).doit() == sm.vector.divergence(u_field)
nabla.cross(u_field)
nabla.cross(u_field).doit() == sm.vector.curl(u_field)
nabla(f_field)
nabla(f_field).doit() == sm.vector.gradient(f_field)
```

    True

```python
#### cylindrical CoordSys3D ####
#### 원통 좌표계
B = sm.vector.CoordSys3D('B')
C = B.create_new('C', transformation='cylindrical')

#### (r,theta,z) -> (x,y.z)
# (r x cos(theta), r x sin(theta), z)
#rv = C.transformation_from_parent()
ra = [ C.r * sm.cos(C.theta), C.r* sm.sin(C.theta), C.z]
rm = sm.Matrix(ra)
rv = sm.vector.matrix_to_vector(rm,C)

#### (r,theta,z) -> (sqrt(x^2 + y^2), atan(y\x), z)
#r_v = C.transformation_from_parent()
####  r > 0, 0 < theta <  2{\pi} -oo < z < oo
r_a = [ sm.sqrt(B.x**2 + B.y**2), sm.atan(B.y/B.x), B.z]
r_m = sm.Matrix(r_a)
r_v = sm.vector.matrix_to_vector(r_m, C)

#### Scale factor Jacobian(행열식) ####
hr = sm.sqrt(rv.diff(C.r).dot(rv.diff(C.r))).simplify()
ht = sm.sqrt(rv.diff(C.theta).dot(rv.diff(C.theta))).simplify()
hz = sm.sqrt(rv.diff(C.z).dot(rv.diff(C.z))).simplify()
#### bases
rh = rv.diff(C.r)/hr
th = rv.diff(C.theta)/ht
zh = rv.diff(C.z)/hz
rh.cross(th).simplify() == zh

#### dS = d{\theta} * dz + 2*dr
## dS = ht * hz * d{\theta}*dz + 2(hr*dr)
## dS = r*d{\theta}*dz + 2(dr)
sm.Integral(C.r,(C.theta,0,2*sm.pi),(C.z,0,C.z)) + 2*sm.Integral(C.r,(C.theta,0,2*sm.pi))
sm.integrate(C.r,(C.theta,0,2*sm.pi),(C.z,0,C.z)) + 2*sm.integrate(C.r,(C.theta,0,2*sm.pi))
sm.integrate(ht*hz,(C.theta,0,2*sm.pi),(C.z,0,C.z)) + 2*sm.integrate(ht,(C.theta,0,2*sm.pi))

#### dV
## dV = hr * ht * hz * dr * dt * dz = dr * r*dt * dz
## dV = dr * r*d{\theta} * dz
sm.Integral(hr*ht*hz,(C.z,0,C.z),(C.theta,0,2*sm.pi),(C.r,0,C.r))
sm.integrate(hr*ht*hz,(C.z,0,C.z),(C.theta,0,2*sm.pi),(C.r,0,C.r))
sm.integrate(C.r,(C.z,0,C.z),(C.theta,0,2*sm.pi),(C.r,0,C.r))

sm.Matrix(C.transformation_to_parent()).subs({C.i:sm.cos(C.theta),C.j:sm.sin(C.theta)})
uf = C.r*sm.cos(C.theta)**2*C.i + C.r*sm.sin(C.theta)**2*C.j + C.z*C.k
type(uf)
uf.to_matrix(C)

sf = C.r**2
type(sf)

nabla = sm.vector.Del()

nabla(sf).doit() == sm.vector.gradient(sf)
nabla.dot(uf).doit() == sm.vector.divergence(uf)
nabla.cross(uf).doit() == sm.vector.curl(uf)
nabla.cross(uf).doit()

```

$\displaystyle (\frac{2 \mathbf{{r}_{C}} \sin^{2}{\left(\mathbf{{theta}_{C}} \right)} + 2 \mathbf{{r}_{C}} \sin{\left(\mathbf{{theta}_{C}} \right)} \cos{\left(\mathbf{{theta}_{C}} \right)}}{\mathbf{{r}_{C}}})\mathbf{\hat{k}_{C}}$

```python
#### spherical CoordSys3D #####
#### 구 좌표계 ####
B = sm.vector.CoordSys3D('B')
S = B.create_new('S', transformation='spherical')
u = S.r*sm.sin(S.theta)*sm.cos(S.phi)*S.i + S.r*sm.sin(S.theta)*sm.sin(S.phi)*S.j + S.r*sm.cos(S.theta)
u.to_matrix(S)
sm.vector.matrix_to_vector(sm.Matrix(S.transformation_to_parent()),S)

### \ver{r} (r,theta,phi) -> (x,y,z)
ra = [S.r*sm.sin(S.theta)*sm.cos(S.phi), S.r*sm.sin(S.theta)*sm.sin(S.phi),S.r*sm.cos(S.theta)]
rm = sm.Matrix(ra)
rv = sm.vector.matrix_to_vector(rm,S)

#### (x,y,z) ->  (r, theta, phi)
r_a = [sm.sqrt(B.x**2 + B.y**2 + B.z**2),
       sm.atan(sm.sqrt(B.x**2 + B.y**2)/B.z),
       sm.atan(B.y/B.x)]
r_m = sm.Matrix(r_a)
r_x = sm.vector.matrix_to_vector(r_m,S)
#print(f'{r_x}\n{sm.vector.matrix_to_vector(sm.Matrix(S.transformation_from_parent()),S)}')

### height : length r theta phi
##
#hr = rv.diff(S.r).magnitude().doit().simplify()
hr = rv.diff(S.r)
print(f'{hr}')
hr = sm.sqrt(hr.dot(hr)).simplify()
print(f'dr \n{hr} \n{rv.diff(S.r)}')
ht = rv.diff(S.theta)
ht = sm.sqrt(ht.dot(ht)).simplify()

hp = rv.diff(S.phi)
hp = sm.sqrt(hp.dot(hp)).simplify()

rh = rv.diff(S.r) / hr
th = rv.diff(S.theta) / ht
ph = rv.diff(S.phi) / hp

rh.dot(th).doit().simplify()
rh.dot(ph).doit().simplify()
th.dot(ph).doit().simplify()

rh.cross(th).simplify()
th.cross(ph).simplify()

### dS ###
## dS = d{\theta } * d{\phi}
## dr/{\partial \theta}.magnitude() => r * d{\theta}
#print(f'D_theta= {ht.simplify()}')
## dr/{\partial \phi}.magnitude() /phi 축 방향 크기 => r * sin(\theta) d{\phi}
#print(f'D_phi = {hp.simplify()}')
# r^2 x sin({\theta})
# dS = ht x hp x dt x dp = r^2 x sin(theta) x d{\theta} x d{\phi}
sm.Integral(S.r**2 * sm.sin(S.theta),(S.theta,0,sm.pi),(S.phi,0,2*sm.pi))
sm.integrate(S.r**2 * sm.sin(S.theta),(S.theta,0,sm.pi),(S.phi,0,2*sm.pi))

### dV ###
## dV = dr x d{\theta} x d{\phi}
## dr = 1
#print(f'D_r = {hr.simplify()}')
rv.diff(S.r)
sm.Integral(hr*ht*hp,(S.r,0,2*sm.pi),(S.theta,0,sm.pi),(S.phi,0,2*sm.pi))
sm.integrate(hr*ht*hp,(S.r,0,S.r),(S.theta,0,sm.pi),(S.phi,0,2*sm.pi))

### gradient = \nabla(f) ###
nabla = sm.vector.Del()
sm.integrate(S.r**2 * sm.sin(S.theta),(S.theta,0,sm.pi),(S.phi,0,2*sm.pi)) ==  sm.integrate(ht*hp,(S.theta,0,sm.pi),(S.phi,0,2*sm.pi))
```

    (sin(S.theta)*cos(S.phi))*S.i + (sin(S.phi)*sin(S.theta))*S.j + (cos(S.theta))*S.k
    dr
    1
    (sin(S.theta)*cos(S.phi))*S.i + (sin(S.phi)*sin(S.theta))*S.j + (cos(S.theta))*S.k

    True

# Creating New System v.to_matrix(B)
> ## sympy.vector.CoordSys3D('name')

# Transformaing New System
> ## obj.create_new (name, transforamtion='')
>> ## 원점은 변하지 않는다, axis 만 각과 섞어서 쓴다.
>>> name: str,
>>> transformation: lambda,tuple.str('cylindrical','spherical'),
>>> vector_names = ' ',
>>> variable_names = ' '

```python
C = B.create_new('C','cylindrical')
C = B.create_new(name='C',transformation='cylindrical')
D = B.create_new('D',transformation=lambda x,y,z:(sm.cos(x),sm.sin(y),z))

C.base_vectors()
C.base_scalars()

sm.Matrix(C.transformation_from_parent())
sm.Matrix(C.transformation_to_parent())
# wrt = with respect to
#C.position_wrt(B)
sm.vector.express(p,C)
```

```python
x = sm.symbols('x')
expr = abs(sm.sin(x**2))
sm.ccode(expr)
sm.julia_code(expr)
sm.octave_code(expr)
#import sympy.printing.cxxcode
sm.cxxcode(expr)
sm.ccode?
```

    Signature: sm.ccode(expr, assign_to=None, standard='c99', **settings)
    Docstring:
    Converts an expr to a string of c code

    Parameters
    ==========

    expr : Expr
        A sympy expression to be converted.
    assign_to : optional
        When given, the argument is used as the name of the variable to which
        the expression is assigned. Can be a string, ``Symbol``,
        ``MatrixSymbol``, or ``Indexed`` type. This is helpful in case of
        line-wrapping, or for expressions that generate multi-line statements.
    standard : str, optional
        String specifying the standard. If your compiler supports a more modern
        standard you may set this to 'c99' to allow the printer to use more math
        functions. [default='c89'].
    precision : integer, optional
        The precision for numbers such as pi [default=17].
    user_functions : dict, optional
        A dictionary where the keys are string representations of either
        ``FunctionClass`` or ``UndefinedFunction`` instances and the values
        are their desired C string representations. Alternatively, the
        dictionary value can be a list of tuples i.e. [(argument_test,
        cfunction_string)] or [(argument_test, cfunction_formater)]. See below
        for examples.
    dereference : iterable, optional
        An iterable of symbols that should be dereferenced in the printed code
        expression. These would be values passed by address to the function.
        For example, if ``dereference=[a]``, the resulting code would print
        ``(*a)`` instead of ``a``.
    human : bool, optional
        If True, the result is a single string that may contain some constant
        declarations for the number symbols. If False, the same information is
        returned in a tuple of (symbols_to_declare, not_supported_functions,
        code_text). [default=True].
    contract: bool, optional
        If True, ``Indexed`` instances are assumed to obey tensor contraction
        rules and the corresponding nested loops over indices are generated.
        Setting contract=False will not generate loops, instead the user is
        responsible to provide values for the indices in the code.
        [default=True].

    Examples
    ========

    >>> from sympy import ccode, symbols, Rational, sin, ceiling, Abs, Function
    >>> x, tau = symbols("x, tau")
    >>> expr = (2*tau)**Rational(7, 2)
    >>> ccode(expr)
    '8*M_SQRT2*pow(tau, 7.0/2.0)'
    >>> ccode(expr, math_macros={})
    '8*sqrt(2)*pow(tau, 7.0/2.0)'
    >>> ccode(sin(x), assign_to="s")
    's = sin(x);'
    >>> from sympy.codegen.ast import real, float80
    >>> ccode(expr, type_aliases={real: float80})
    '8*M_SQRT2l*powl(tau, 7.0L/2.0L)'

    Simple custom printing can be defined for certain types by passing a
    dictionary of {"type" : "function"} to the ``user_functions`` kwarg.
    Alternatively, the dictionary value can be a list of tuples i.e.
    [(argument_test, cfunction_string)].

    >>> custom_functions = {
    ...   "ceiling": "CEIL",
    ...   "Abs": [(lambda x: not x.is_integer, "fabs"),
    ...           (lambda x: x.is_integer, "ABS")],
    ...   "func": "f"
    ... }
    >>> func = Function('func')
    >>> ccode(func(Abs(x) + ceiling(x)), standard='C89', user_functions=custom_functions)
    'f(fabs(x) + CEIL(x))'

    or if the C-function takes a subset of the original arguments:

    >>> ccode(2**x + 3**x, standard='C99', user_functions={'Pow': [
    ...   (lambda b, e: b == 2, lambda b, e: 'exp2(%s)' % e),
    ...   (lambda b, e: b != 2, 'pow')]})
    'exp2(x) + pow(3, x)'

    ``Piecewise`` expressions are converted into conditionals. If an
    ``assign_to`` variable is provided an if statement is created, otherwise
    the ternary operator is used. Note that if the ``Piecewise`` lacks a
    default term, represented by ``(expr, True)`` then an error will be thrown.
    This is to prevent generating an expression that may not evaluate to
    anything.

    >>> from sympy import Piecewise
    >>> expr = Piecewise((x + 1, x > 0), (x, True))
    >>> print(ccode(expr, tau, standard='C89'))
    if (x > 0) {
    tau = x + 1;
    }
    else {
    tau = x;
    }

    Support for loops is provided through ``Indexed`` types. With
    ``contract=True`` these expressions will be turned into loops, whereas
    ``contract=False`` will just print the assignment expression that should be
    looped over:

    >>> from sympy import Eq, IndexedBase, Idx
    >>> len_y = 5
    >>> y = IndexedBase('y', shape=(len_y,))
    >>> t = IndexedBase('t', shape=(len_y,))
    >>> Dy = IndexedBase('Dy', shape=(len_y-1,))
    >>> i = Idx('i', len_y-1)
    >>> e=Eq(Dy[i], (y[i+1]-y[i])/(t[i+1]-t[i]))
    >>> ccode(e.rhs, assign_to=e.lhs, contract=False, standard='C89')
    'Dy[i] = (y[i + 1] - y[i])/(t[i + 1] - t[i]);'

    Matrices are also supported, but a ``MatrixSymbol`` of the same dimensions
    must be provided to ``assign_to``. Note that any expression that can be
    generated normally can also exist inside a Matrix:

    >>> from sympy import Matrix, MatrixSymbol
    >>> mat = Matrix([x**2, Piecewise((x + 1, x > 0), (x, True)), sin(x)])
    >>> A = MatrixSymbol('A', 3, 1)
    >>> print(ccode(mat, A, standard='C89'))
    A[0] = pow(x, 2);
    if (x > 0) {
       A[1] = x + 1;
    }
    else {
       A[1] = x;
    }
    A[2] = sin(x);
    File:      ~/.local/lib/python3.9/site-packages/sympy/printing/codeprinter.py
    Type:      function

```python
S = B.create_new('S','spherical')
S.to_matrix()
```

# Locating New System
> # obj.locate_new(...)
>> name,
>> postition,
>> vector_names,
>> variable_names

```python
L = B.locate_new('L', 5*B.i+ 2*B.j + 3*B.k)
sm.Matrix(L.transformation_to_parent()).subs({L.x:7,L.y:2,L.z:3})
sm.Matrix(L.transformation_to_parent())
L.position_wrt(B)
# something wrong???
sm.vector.express(2*L.i+L.j+L.k,B)
```

$\displaystyle (2)\mathbf{\hat{i}_{B}} + \mathbf{\hat{j}_{B}} + \mathbf{\hat{k}_{B}}$

# Orienting New System
> ## 한축을 중심으로 나머지 origin 회전시킨다..

> # obj.orient_new(..)
> ## 축의 각도가 변한다.
>> ### obj.orient_new_axis
>>> ### B.orient_new_axis(
        name,
        angle,
        axis vector,
        locastion=$\vec{v}$
)
>> ### obj.orient_new_body
>> ### obj.orient_new_space
>> ### obj.orient_new_quaternion

# QuaternionOrienter
## obj.orient_new(name,(,))v

```python
theta = sm.symbols('theta')

# \hat{k}를 축으로 해서 \theta 만큼 회전시킨다.
N = B.orient_new_axis('N', theta, B.k)

sm.Matrix(N.transformation_to_parent())
#sm.Matrix(N.transformation_to_parent()).subs({theta:sm.pi/4,N.x:1,N.y:2,N.z:2})
# sm.vector.express(p,N)
```

$\displaystyle \left[\begin{matrix}\mathbf{{x}_{N}} \cos{\left(\theta \right)} + \mathbf{{y}_{N}} \sin{\left(\theta \right)}\\- \mathbf{{x}_{N}} \sin{\left(\theta \right)} + \mathbf{{y}_{N}} \cos{\left(\theta \right)}\\\mathbf{{z}_{N}}\end{matrix}\right]$

```python
N.rotation_matrix(B)
N.rotation_matrix(B).subs({theta:sm.pi/4})
N.rotation_matrix(B).subs({theta:sm.pi/4}).dot([1,2,2])
#sm.Matrix(N.rotation_matrix(B).subs({theta:sm.pi/4}).dot([1,2,2]))
```

    [3*sqrt(2)/2, sqrt(2)/2, 2]

# Orienting and Locating New Coordinate System
> ## B.orient_new_axis (
>> ### 'name',
>> ### $(\angle)$ angle scalar, $(\vec{axis})$ axis vector,
>> ### location = $(\vec{move})$ vector
> ## )

```python
a,b,r = sm.symbols('alpha beta gamma')
M = B.orient_new_axis('M',theta,B.k,location= a*B.i + b*B.j + r*B.k)
sm.Matrix(M.transformation_to_parent()).subs({theta:sm.pi/4,a:1,b:1,r:1,M.x:1,M.y:2,M.z:2})
```

$\displaystyle \left[\begin{matrix}1 + \frac{3 \sqrt{2}}{2}\\\frac{\sqrt{2}}{2} + 1\\3\end{matrix}\right]$

# quaternion
> ## sympy.algebras.quaternion
>> ## $q = (w , \vec{v})$
>>> ## $q = (w,(x,y,z))$
>>> ## $q = w + xi + yj + zk \\ q_1 = w_1 + \vec{v_1} \\ q_2= w_2 + \vec{v_2}$
> ## $q_1\:q_2 = (w_1w_2 - \vec{v_1}\cdot \vec{v_2}, \quad w_1\vec{v_1} + w_2\vec{v_2} + \vec{v_1}\times\vec{v_2})$
> ## $q^2 = (0,\vec{v})(0,\vec{v}) = (-\vec{v}\cdot \vec{v}, \vec{0}) = -|\vec{v}|^2 $

```python
import sympy.algebras
x = sm.symbols('x')
q = sm.algebras.Quaternion(1,2,3,4)
q1 = sm.algebras.Quaternion(x,x**3,x,x**2,real_field=False)
q2 = sm.algebras.Quaternion(3+4*sm.I,2+5*sm.I,0,7+8*sm.I,real_field=False)
q2
```

$\displaystyle \left(3 + 4 i\right) + \left(2 + 5 i\right) i + 0 j + \left(7 + 8 i\right) k$

#
> ### $ \vec{v} = ai + bj + ck \\
q = w + \vec{v} \\
\quad = (w, \vec{v}) \\
\quad = (w, ai + bj + ck) $
>> ### $ e^{ai + bj + ck} = cos(|\vec{v}|) +
\frac{sin(|\vec{v}|}{|\vec{v}|}
(ai + bj + ck)$
>> ### $ e^{w + ai + bj + ck} \\
\quad = e^{w} e^{ai+bj+ck} \\
\quad = cos(|\vec{v}|) +  \frac{sin(|\vec{v}|}{|\vec{v}|}
(ai + bj + ck) \\
\quad = cos(|\vec{v}|) + sin(|\vec{v}|)\,
\frac {ai + bj + ck}{|\vec{v}|} \\
\quad = cos(|\vec{v}|) + sin(|\vec{v}|)\,
\frac {\vec{v}}{|\vec{v}|} \\
\quad = cos(|\vec{v}|) + sin(|\vec{v}|)\,\hat{v}\\
$
> ## $ e^{\vec{v}} = e^{ai+bj+ck}
\begin{cases}
cos(|\vec{v}|) +
\frac{sin(|\vec{v}|}{|\vec{v}|}\,(ai + bj + ck) \\
cos(|\vec{v}|) +
sin(|\vec{v}|)\,
(
\frac{ai}{|\vec{v}|} +
\frac{bj}{|\vec{v}|} +
\frac{ck}{|\vec{v}|}
) \\
cos(|\vec{v}|) + sin(|\vec{v}|)\,\hat{v} \\
\end{cases}$

```python
# e^q
import sympy.algebras
theta = sm.symbols('theta')
q = sm.algebras.Quaternion(sm.cos(theta),
                           sm.sin(theta)*sm.sqrt(1/3),
                           sm.sin(theta)*sm.sqrt(1/3),
                           sm.sin(theta)*sm.sqrt(1/3))
q*q
sm?

```

    Type:        module
    String form: <module 'sympy' from '/home/jkarng/.local/lib/python3.9/site-packages/sympy/__init__.py'>
    File:        ~/.local/lib/python3.9/site-packages/sympy/__init__.py
    Docstring:
    SymPy is a Python library for symbolic mathematics. It aims to become a
    full-featured computer algebra system (CAS) while keeping the code as simple
    as possible in order to be comprehensible and easily extensible.  SymPy is
    written entirely in Python. It depends on mpmath, and other external libraries
    may be optionally for things like plotting support.

    See the webpage for more information and documentation:

        https://sympy.org

```python
sm.exp(sm.algebras.Quaternion(1,1,1,1))
```

```python
sm.algebras.Quaternion(1,1,1,1).exp()
```

# Quaternion Multiply
> #[quaternion](https://www.youtube.com/watch?v=88BA8aO3qXA)
> $ (a + bi + cj + dk)\:(e + fi + gj + hk ) \\
\begin{bmatrix}
a & -b & -c & -d \\
b &  a & -d & c \\
c &  d &  a & -b \\
d & -c &  b & a
\end{bmatrix} \quad
\begin{bmatrix}
e \\ f \\ g \\ h
\end{bmatrix}
$
> # unit quaternion matrix
>> $ 1 = (1,0,0,0) \\
\begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
$
>> $ i = (0,1,0,0) \\
\begin{bmatrix}
0 & -1 & 0 & 0  \\
1 & 0 & 0 & 0 \\
0 & 0 & 0 & -1 \\
0 & 0 & 1 & 0 \\
\end{bmatrix}
$
>> $ j = (0,0,1,0) \\
\begin{bmatrix}
0 & 0 & -1 & 0 \\
0 & 0 & 0 & -1 \\
1 & 0 & 0 & 0 \\
0 & -1 & 0 & 0 \\
\end{bmatrix}
$
>> $k = (0,0,0,1) \\
\begin{bmatrix}
0 & 0 & 0 & -1 \\
0 & 0 & -1 & 0 \\
0 & 1 & 0 & 0 \\
1 & 0 & 0 & 0 \\
\end{bmatrix}
$

```python
q0, q1, q2, q3 = sm.symbols('q_0 q_1 q_2 q_3')
Q = B.orient_new_quaternion('Q',q0,q1,q2,q3)
sm.Matrix(Q.transformation_to_parent())
```

```python
m = sm.Matrix([1,2,3])
q = sm.algebras.Quaternion(1,2,3,4)

q.norm()
q.normalize()
q.inverse()
q.pow(2) == q.mul(q) == q*q
sm.vector.divergence(q)
sm.vector.curl(q)
sm.vector.gradient(q)
```

```python
x = sm.symbols('x')
q1 = sm.algebras.Quaternion(x**2,x**3,x)
q2 = sm.algebras.Quaternion(2,(3+2*sm.I), x**2, 3.5*sm.I)
```

```python
q1 * q2
```

```python
q1.inverse()
q1.conjugate()/q1.norm()
```

```python
q1.inverse()
```

```python

```


# Protein binding & undfolding – a four-state model
In this notebook we will look into a the kinetics of a model system describing competing protein folding, aggregation and ligand binding. Using ChemPy we can define thermodynamic and kinetic parameters, and obtain
a representation of a system of ODEs which may be integrated efficiently. Since we use SymPy we can also
generate publication quality latex-expressions of our mathematical model directly from our source code. No need to write the equations multiple times in Python/Latex (or even C++ if the integration is to be performed a large number of times such as during parameter estimation).

First we will perform our imports:

```python
from collections import OrderedDict, defaultdict
import math
import re
import time
from IPython.display import Image, Latex, display
import matplotlib.pyplot as plt
import sympy
from pyodesys.symbolic import ScaledSys
from pyodesys.native.cvode import NativeCvodeSys
from chempy import Substance, Equilibrium, Reaction, ReactionSystem
from chempy.kinetics.ode import get_odesys
from chempy.kinetics.rates import MassAction
from chempy.printing.tables import UnimolecularTable, BimolecularTable
from chempy.thermodynamics.expressions import EqExpr
from chempy.util.graph import rsys2graph
from chempy.util.pyutil import defaultkeydict
%matplotlib inline
```

Next we will define our substances. Note how we specify the composition, this will allow ChemPy to raise an error if any of our reactions we enter later would violate mass-conservation. It will also allow us to reduce the number of unknowns in our ODE-system by using the linear invariants from the mass-conservation.

```python
substances = OrderedDict([
    ('N', Substance('N', composition={'protein': 1}, latex_name='[N]')),
    ('U', Substance('U', composition={'protein': 1}, latex_name='[U]')),
    ('A', Substance('A', composition={'protein': 1}, latex_name='[A]')),
    ('L', Substance('L', composition={'ligand': 1}, latex_name='[L]')),
    ('NL', Substance('NL', composition={'protein': 1, 'ligand': 1}, latex_name='[NL]')),
])
```

We will model thermodynamic properties using enthalpy (H), entropy (S) and heat capacity (Cp). Kinetic paramaters (rate constants) are assumed to follow the Eyring equation:

```python
def _gibbs(args, T, R, backend, **kwargs):
    H, S, Cp, Tref = args
    H2 = H + Cp*(T - Tref)
    S2 = S + Cp*backend.log(T/Tref)
    return backend.exp(-(H2 - T*S2)/(R*T))

def _eyring(args, T, R, k_B, h, backend, **kwargs):
    H, S = args
    return k_B/h*T*backend.exp(-(H - T*S)/(R*T))
```

```python
Gibbs = EqExpr.from_callback(_gibbs, parameter_keys=('temperature', 'R'), argument_names=('H', 'S', 'Cp', 'Tref'))
Eyring = MassAction.from_callback(_eyring, parameter_keys=('temperature', 'R', 'k_B', 'h'), argument_names=('H', 'S'))
```

Next we define our free parameters:

```python
thermo_dis = Gibbs(unique_keys=('He_dis', 'Se_dis', 'Cp_dis', 'Tref_dis'))
thermo_u = Gibbs(unique_keys=('He_u', 'Se_u', 'Cp_u', 'Tref_u'))  # ([He_u_R, Se_u_R, Cp_u_R, Tref])
kinetics_agg = Eyring(unique_keys=('Ha_agg', 'Sa_agg'))  # EyringMassAction([Ha_agg, Sa_agg])
kinetics_as = Eyring(unique_keys=('Ha_as', 'Sa_as'))
kinetics_f = Eyring(unique_keys=('Ha_f', 'Sa_f'))
```

We will have two reversible reactions, and one irreversible reaction:

```python
eq_dis = Equilibrium({'NL'}, {'N', 'L'}, thermo_dis, name='ligand-protein dissociation')
eq_u = Equilibrium({'N'}, {'U'}, thermo_u, {'L'}, {'L'}, name='protein unfolding')
r_agg = Reaction({'U'}, {'A'}, kinetics_agg, {'L'}, {'L'}, name='protein aggregation')
```

We formulate a system of 5 reactions honoring our reversible equilibria and our irreversible reaction:

```python
rsys = ReactionSystem(
    eq_dis.as_reactions(kb=kinetics_as, new_name='ligand-protein association') +
    eq_u.as_reactions(kb=kinetics_f, new_name='protein folding') +
    (r_agg,), substances, name='4-state CETSA system')
```

We can query the ``ReactionSystem`` instance for what substances contain what components:

```python
vecs, comp = rsys.composition_balance_vectors()
names = rsys.substance_names()
dict(zip(comp, [dict(zip(names, v)) for v in vecs]))
```

We can look at our ``ReactionSystem`` as a graph if we wish:

```python
rsys2graph(rsys, '4state.png', save='.', include_inactive=False)
Image('4state.png')
```

...or as a Table if that suits us better (note that "A" ha green highlighting, denoting it's a terminal product)

```python
rsys
```

Try hovering over the names to have them highlighted (this is particularly useful when working with large reaction sets).

We ca also generate tables representing the unimolecular reactions involing each substance, or the matrix showing the bimolecular reactions:

```python
uni, not_uni = UnimolecularTable.from_ReactionSystem(rsys)
bi, not_bi = BimolecularTable.from_ReactionSystem(rsys)
assert not (not_bi & not_uni), "Only uni- & bi-molecular reactions expected"
```

```python
uni
```

```python
bi
```

Exporting expressions as LaTeX is quite straightforward:

```python
def pretty_replace(s, subs=None):
    if subs is None:
        subs = {
            'Ha_(\w+)': r'\Delta_{\1}H^{\\neq}',
            'Sa_(\w+)': r'\Delta_{\1}S^{\\neq}',
            'He_(\w+)': r'\Delta_{\1}H^\circ',
            'Se_(\w+)': r'\Delta_{\1}S^\circ',
            'Cp_(\w+)': r'\Delta_{\1}\,C_p',
            'Tref_(\w+)': r'T^{\circ}_{\1}',
        }
    for pattern, repl in subs.items():
        s = re.sub(pattern, repl, s)
    return s

def mk_Symbol(key):
    if key in substances:
        arg = substances[key].latex_name
    else:
        arg = pretty_replace(key.replace('temperature', 'T'))

    return sympy.Symbol(arg)

autosymbols = defaultkeydict(mk_Symbol)
rnames = {}
for rxn in rsys.rxns:
    rnames[rxn.name] = rxn.name.replace(' ', '~').replace('-','-')
    rate_expr_str = sympy.latex(rxn.rate_expr()(autosymbols, backend=sympy, reaction=rxn))
    lstr = r'$r(\mathrm{%s}) = %s$' % (rnames[rxn.name], rate_expr_str)
    display(Latex(lstr))
```

```python
ratexs = [autosymbols['r(\mathrm{%s})' % rnames[rxn.name]] for rxn in rsys.rxns]
rates = rsys.rates(autosymbols, backend=sympy, ratexs=ratexs)
for k, v in rates.items():
    display(Latex(r'$\frac{[%s]}{dt} = %s$' % (k, sympy.latex(v))))
```

```python
default_c0 = defaultdict(float, {'N': 1e-9, 'L': 1e-8})

params = dict(
    R=8.314472,  # or N_A & k_B
    k_B=1.3806504e-23,
    h=6.62606896e-34,  # k_B/h == 2.083664399411865e10 K**-1 * s**-1
    He_dis=-45e3,
    Se_dis=-400,
    Cp_dis=1.78e3,
    Tref_dis=298.15,
    He_u=60e3,
    Cp_u=20.5e3,
    Tref_u=298.15,
    Ha_agg=106e3,
    Sa_agg=70,
    Ha_as=4e3,
    Sa_as=-10,
    Ha_f=90e3,
    Sa_f=50,
    temperature=50 + 273.15
)

```

We have the melting temperature $T_m$ as a free parameter, however, the model is expressed in terms of $\Delta_u S ^\circ$ so will need to derive the latter from the former:
$$
\begin{cases}
\Delta G = 0 \\
\Delta G = \Delta H - T_m\Delta_u S
\end{cases}
$$
$$
\begin{cases}
\Delta H = \Delta H^\circ + \Delta C_p \left( T_m - T^\circ \right) \\
\Delta S = \Delta S^\circ + \Delta C_p \ln\left( \frac{T_m}{T^\circ} \right)
\end{cases}
$$
this gives us the following equation:
$$
\Delta H^\circ + \Delta C_p \left( T_m - T^\circ \right) = T_m \left( \Delta S^\circ + \Delta C_p \ln\left( \frac{T_m}{T^\circ} \right) \right)
$$
Solving for $\Delta S^\circ$:
$$
\Delta S^\circ = T_m^{-1}\left( \Delta H^\circ + \Delta C_p \left( T_m - T^\circ \right) \right) - \Delta C_p \ln\left( \frac{T_m}{T^\circ} \right)
$$

```python
def Se0_from_Tm(Tm, token):
    dH0, T0, dCp = params['He_'+token], params['Tref_'+token], params['Cp_'+token]
    return dH0/Tm + (Tm-T0)*dCp/Tm - dCp*math.log(Tm/T0)
params['Se_u'] = Se0_from_Tm(48.2+273.15, 'u')
params['Se_u']
```

If we want to see the numerical values for the rate of the individual reactions it is quite easy:

```python
params_c0 = default_c0.copy()
params_c0.update(params)
for rxn in rsys.rxns:
    print('%s: %.5g' % (rxn.name, rxn.rate_expr()(params_c0, reaction=rxn)))
```

By using [pyodesys](https://github.com/bjodah/pyodesys) we can generate a system of ordinary differential equations:

```python
odesys, extra = get_odesys(rsys, include_params=False, SymbolicSys=ScaledSys, dep_scaling=1e9)
len(odesys.exprs)  # how many (symbolic) expressions are there in this representation?
```

Numerical integration of ODE systems require a guess for the inital step-size. We can derive an upper bound for an "Euler-forward step" from initial concentrations and restrictions on mass-conservation:

```python
h0max = extra['max_euler_step_cb'](0, default_c0, params)
h0max
```

Now let's put our ODE-system to work:

```python
def integrate_and_plot(system, c0=None, first_step=None, t0=0, stiffness=False, nsteps=9000, **kwargs):
    if c0 is None:
        c0 = default_c0
    if first_step is None:
        first_step = h0max*1e-11
    tend = 3600*24
    t_py = time.time()

    kwargs['atol'] = kwargs.get('atol', 1e-11)
    kwargs['rtol'] = kwargs.get('rtol', 1e-11)
    res = system.integrate([t0, tend], c0, params, integrator='cvode', nsteps=nsteps,
                           first_step=first_step, **kwargs)
    t_py = time.time() - t_py
    if stiffness:
        plt.subplot(1, 2, 1)
    _ = system.plot_result(xscale='log', yscale='log')
    _ = plt.legend(loc='best')
    plt.gca().set_ylim([1e-16, 1e-7])
    plt.gca().set_xlim([1e-11, tend])

    if stiffness:
        if stiffness is True:
            stiffness = 0
        ratios = odesys.stiffness()
        plt.subplot(1, 2, 2)
        plt.yscale('linear')
        plt.plot(odesys._internal[0][stiffness:], ratios[stiffness:])
    for k in ('time_wall', 'time_cpu'):
        print('%s = %.3g' % (k, res[2][k]), end=', ')
    print('time_python = %.3g' % t_py)
    return res

_, _, info = integrate_and_plot(odesys)
assert info['internal_yout'].shape[1] == 5
{k: v for k, v in info.items() if not k.startswith('internal')}
```

``pyodesys`` even allows us to generate C++ code which is compiled to a fast native extension module:

```python
native = NativeCvodeSys.from_other(odesys, first_step_expr=0*odesys.indep)
```

```python
_, _, info_native = integrate_and_plot(native)
{k: v for k, v in info_native.items() if not k.startswith('internal')}
```

Note how much smaller "time_cpu" was here

```python
info['time_wall']/info_native['time_wall']
```

```python
from chempy.kinetics._native import get_native
```

```python
native2 = get_native(rsys, odesys, 'cvode')
```

```python
_, _, info_native2 = integrate_and_plot(native2, first_step=0.0)
{k: v for k, v in info_native2.items() if not k.startswith('internal')}
```

We have one complication, due to linear dependencies in our formulation of the system of ODEs our jacobian is singular:

```python
cses, (jac_in_cse,) = odesys.be.cse(odesys.get_jac())
jac_in_cse
```

```python
odesys.jacobian_singular()
```

Since implicit methods (which are required for stiff cases often encountered in kinetic modelling) uses the Jacboian (or rather **I - γJ**) in the modified Newton's method we may get failures during integration (depending on step size and scaling). What we can do is to identify linear dependencies based on composition of the materials and exploit the invariants to reduce the dimensionality of the system of ODEs:

```python
A, comp_names = rsys.composition_balance_vectors()
A, comp_names, list(rsys.substances.keys())
```

That made sense: two different components can give us (up to) two linear invariants.

Let's look what those invariants looks like symbolically:

```python
y0 = {odesys[k]: sympy.Symbol(k+'0') for k in rsys.substances.keys()}
analytic_L_N = extra['linear_dependencies'](['L', 'N'])
analytic_L_N(None, y0, None, sympy)
assert len(analytic_L_N(None, y0, None, sympy)) > 0  # ensure the callback is idempotent
analytic_L_N(None, y0, None, sympy), list(enumerate(odesys.names))
```

one can appreciate that one does not need to enter such expressions manually (at least for larger systems). That is both tedious and error prone.

Let's see how we can use ``pyodesys`` to leverage this information on redundancy:

```python
from pyodesys.symbolic import PartiallySolvedSystem
no_invar = dict(linear_invariants=None, linear_invariant_names=None)
psysLN = PartiallySolvedSystem(odesys, analytic_L_N, **no_invar)
print(psysLN.be.cse(psysLN.get_jac())[1][0])
psysLN['L'], psysLN.jacobian_singular(), len(psysLN.exprs)
```

above we chose to get rid of 'L' and 'N', but we could also have removed 'A' instead of 'N':

```python
psysLA = PartiallySolvedSystem(odesys, extra['linear_dependencies'](['L', 'A']), **no_invar)
print(psysLA.be.cse(psysLA.get_jac())[1][0])
psysLA['L'], psysLA.jacobian_singular()
```

```python
plt.figure(figsize=(12,4))
plt.subplot(1, 2, 1)
_, _, info_LN = integrate_and_plot(psysLN, first_step=0.0)
assert info_LN['internal_yout'].shape[1] == 3

plt.subplot(1, 2, 2)
_, _, info_LA = integrate_and_plot(psysLA, first_step=0.0)
assert info_LA['internal_yout'].shape[1] == 3

({k: v for k, v in info_LN.items() if not k.startswith('internal')},
 {k: v for k, v in info_LA.items() if not k.startswith('internal')})
```

We can also have the solver return to use when some precondition is fulfilled, e.g. when the concentraion of 'N' and 'A' are equal:

```python
from pyodesys.symbolic import SymbolicSys
psys_root = SymbolicSys.from_other(psysLN, roots=[psysLN['N'] - psysLN['A']])
```

```python
psys_root.roots
```

```python
psysLN['N']
```

```python
psysLN.analytic_exprs
```

```python
psysLN.names
```

```python
psysLN.dep
```

```python
tout1, Cout1, info_root = integrate_and_plot(psys_root, first_step=0.0, return_on_root=True)
print('Time at which concnetrations of N & A are equal: %.4g' % (tout1[-1]))
```

From this point in time onwards we could for example choose to continue our integration using another formulation of the ODE-system:

```python
xout2, yout2, info_LA = integrate_and_plot(psysLA, first_step=0.0, t0=tout1[-1], c0=dict(zip(odesys.names, Cout1[-1, :])))
```

Let's compare the total number steps needed for our different approaches:

```python
print('\troot\tLA\troot+LA\tLN')
for k in 'n_steps nfev njev'.split():
    print('\t'.join(map(str, (k, info_root[k], info_LA[k], info_root[k] + info_LA[k], info_LN[k]))))
```

In this case it did not earn us much, one reason is that we actually don't need to find the root with as high accuracy as we do. But having the option is still useful.

Using ``pyodesys`` and SymPy we can perform a variable transformation and solve the transformed system if we so wish:

```python
from pyodesys.symbolic import symmetricsys
logexp = lambda x: sympy.log(x + 1e-20), lambda x: sympy.exp(x) - 1e-20
def psimp(exprs):
    return [sympy.powsimp(expr.expand(), force=True) for expr in exprs]
LogLogSys = symmetricsys(logexp, logexp, exprs_process_cb=psimp)
unscaled_odesys, unscaled_extra = get_odesys(rsys, include_params=False)
tsys = LogLogSys.from_other(unscaled_odesys)
unscaledLN = PartiallySolvedSystem(unscaled_odesys, unscaled_extra['linear_dependencies'](['L', 'N']), **no_invar)
unscaledLA = PartiallySolvedSystem(unscaled_odesys, unscaled_extra['linear_dependencies'](['L', 'A']), **no_invar)
assert sorted(unscaledLN.free_names) == sorted(['U', 'A', 'NL'])
assert sorted(unscaledLA.free_names) == sorted(['U', 'N', 'NL'])
tsysLN = LogLogSys.from_other(unscaledLN)
tsysLA = LogLogSys.from_other(unscaledLA)
```

```python
_, _, info_t = integrate_and_plot(tsys, first_step=0.0)
{k: info_t[k] for k in ('nfev', 'njev', 'n_steps')}
```

We can even apply the transformation our reduced systems (doing so by hand is excessively painful and error prone):

```python
native_tLN = NativeCvodeSys.from_other(tsysLN)
_, _, info_tLN = integrate_and_plot(native_tLN, first_step=1e-9, nsteps=18000, atol=1e-9, rtol=1e-9)
{k: info_tLN[k] for k in ('nfev', 'njev', 'n_steps')}
```

```python
_, _, info_tLN = integrate_and_plot(tsysLN, first_step=1e-9, nsteps=18000, atol=1e-8, rtol=1e-8)
{k: info_tLN[k] for k in ('nfev', 'njev', 'n_steps')}
```

```python
_, _, info_tLA = integrate_and_plot(tsysLA, first_step=0.0)
{k: info_tLA[k] for k in ('nfev', 'njev', 'n_steps')}
```

Finally, let's take a look at the C++ code which was generated for us:

```python
print(open(next(filter(lambda s: s.endswith('.cpp'), native2._native._written_files))).read())
```