# Source code for ahkab.gear

# -*- coding: iso-8859-1 -*-
# gear.py
# Gear Linear Multi-Step (LMS) DF

# This file is part of the ahkab simulator.
#
# Ahkab is free software: you can redistribute it and/or modify
# the Free Software Foundation, version 2 of the License.
#
# Ahkab is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License v2
# along with ahkab.  If not, see <http://www.gnu.org/licenses/>.

# This file should be conforming to a standard

This is an implicit method, it means that to compute dx(n+1)/dt the value of x in (n+1) is required.
We don't know it, since it's our objective).
This method, as all other implicit methods, allows us to write the derivative as:
dx(n+1)/dt = x_coeff * x(n+1) + const                  (ii)

The get_df method returns those two vectors.

Gear's LMS interpolates the solution in a number of points equal to its order. Since it's a implicit
method, one of these is x(n+1). The values x(n), x(n-1)... x(n-(order+2)) need to be supplied to the method.
We can write x(t) as:
x(t)   = a0 + a1*(t(n+1) -  t ) + a2*( t(n+1) - t )^2 + ...        (i)
The equation has <order> a coefficients, which we need to determine.
For this reason, we write a system of "order" equations in this way:
x(n+1) = a0 + a1*(t(n+1) - t(n+1)) + a2*(t(n+1) - t(n+1))^2 + ...
x(n)   = a0 + a1*(t(n+1) -  t(n) ) + a2*( t(n+1) - t(n) )^2 + ...
x(n-1) = a0 + a1*(t(n+1) - t(n-1)) + a2*(t(n+1) - t(n-1))^2 + ...

Which may be rewritten as:

z = A * a

z is the vector of known values of x
A is a time dependant matrix
a is a vector made of the a* coeffiecients

We don't need to explicit ALL of the a* coeffiecients. What we are really looking for is the derivative of x
in t(n+1), dx(n+1)/dt in short.
If we differentiate the relation (i):
dx(t)/dt = -a1 - 2*a2*( t(n+1) - t ) - 3*a3*( t(n+1) - t )^2 ...
Which evaluated in t = t(n+1) gives:
dx(n+1)/dt = -1 * a1

Our objective is then a1.
From the previsious system we write:
a = A^-1 * z
a1 is a[1,0], which may be extracted in this way:
et = [0 1 0 0 0 0 ...] (order elements)
a1 = et * a = et * A^-1 * z
Because of the associative prperty of matrix multiplication, we can write:

P  = et * A^-1
a1 = P[1, :] * z

But, we don't know z[0,0] = x(n+1), we can split the above relation:

a1 = P[1, 0] * x(n+1) + P[1, 1:] * z[1:, 0]

We arrived to the relation written above (ii)
dx(n+1)/dt = x_coeff * x(n+1) + const = -1*a1

So:
x_coeff = -1 * P[1, 0]
const   = -1 * P[1, 1:] * z[1:, 0]

This module uses a faster way to compute the values that doesn't require to invert the matrix.
Anyway, from a theorical point of view, the above applies.
"""

from __future__ import (unicode_literals, absolute_import,
division, print_function)

import numpy as np
import sys

from scipy.misc import factorial

from . import printing

order = None
# FAST = True

[docs]def is_implicit():
return True

[docs]def has_ff():
return True

[docs]def get_required_values():
"""This returns two python arrays built this way:
[ max_order_of_x, max_order_of_dx ]
Where:
Both the values are int, or None
if max_order_of_x is set to k, the df method needs all the x(n-i) values of x,
where i<=k (the value the function assumed i+1 steps before the one we will ask for the derivative).
The same applies to max_order_of_dx, but regards dx(n)/dt
None means that NO value is required.

The first array has to be used if no prediction is required, the second are the values needed for prediction.
"""
# notice that: it returns the same values, it should be order-1 if no prediction is required BUT
# we build the required values in a (hopefully) fast way, that requires
# one point more.
return ((order, None), (order, None))

[docs]def get_df(pv_array, suggested_step, predict=False):
"""The array must be built in this way:
It has to be an array of arrays. Each of them has the following structure:

[time, np_matrix, np_matrix]

Hence the pv_array[k] element is made of:
_ time is the time in which the solution is valid: t(n-k)
_ The first np_matrix is x(n-k)
_ The second is d(x(n-k))/dt
Values that are not needed may be set to None and they will be disregarded.

if predict == True, it needs one more point to give a prediction
of x at the suggested step.

Returns: None if the incorrect values were given, or quits.
Otherwise returns an array:
_ the [0] element is the np matrix of coeffiecients (Nx1) of x(n+1)
_ the [1] element is the np matrix of constant terms (Nx1) of x(n+1)
The derivative may be written as:
d(x(n+1))/dt = ret[0]*x(n+1) + ret[1]"""

if order is None:
printing.print_general_error(
"You must set Gear's order before using it! e.g. gear.order = 5")
sys.exit(1)

s = []
s.append(0)
for index in range(1, order + 2):
s.append(suggested_step + pv_array[0][0] - pv_array[index - 1][0])

# build e[k, i]
e = np.zeros((order + 2, order + 2))
for k_index in range(1, order + 2):
for i_index in range(1, order + 2):
if i_index == k_index:
e[k_index, i_index] = 1
else:
e[k_index, i_index] = s[i_index] / (s[i_index] - s[k_index])

alpha = np.zeros((1, order + 2))
for k_index in range(1, order + 2):
alpha[0, k_index] = 1.0
for j_index in range(order + 1):
alpha[0, k_index] = alpha[0, k_index] * e[k_index, j_index + 1]

# build gamma
gamma = np.zeros((1, order + 1))
for k_index in range(1, order + 1):
gamma[0, k_index] = alpha[0, k_index] * \
((1.0 / s[order + 1]) - (1.0 / s[k_index]))

gamma[0, 0] = 0
for index in range(1, order + 1):
gamma[0, 0] = gamma[0, 0] - gamma[0, index]

# values to be returned
C1 = gamma[0, 0]

C0 = np.zeros(pv_array[0][1].shape)
for index in range(order):
C0 = C0 + gamma[0, index + 1] * pv_array[index][1]

x_lte_coeff = 0
for k_index in range(1, order + 1):
x_lte_coeff = x_lte_coeff + \
(s[k_index] ** (order + 1)) * \
(-1.0 * gamma[0, k_index] / gamma[0, 0])
x_lte_coeff = ((-1.0) ** (order + 1)) * \
(1.0/factorial(order + 1)) * x_lte_coeff

if predict:
predict_x = np.zeros(pv_array[0][1].shape)
for index in range(1, order + 2):  # order
predict_x = predict_x + alpha[0, index] * pv_array[index - 1][1]

predict_lte_coeff = -1.0/factorial(order + 1)
for index in range(1, order + 2):
predict_lte_coeff = predict_lte_coeff * s[index]
# print predict_lte_coeff
# print x_lte_coeff
else:
predict_x = None
predict_lte_coeff = None

return C1, C0, x_lte_coeff, predict_x, predict_lte_coeff