# Copyright 2019 Kristopher L. Kuhlman <klkuhlm _at_ sandia _dot_ gov>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import numba
import numpy as np
[docs]
@numba.njit(nogil=True, cache=True)
def invlap(t, tmax, fp, M, alpha=1e-10, tol=1e-9):
"""Inverse Laplace tansform with algorithm of De Hoog, Knight and Stokes.
Parameters
----------
t : array
times for which inverse is computed
tmax : float
maximum time
fp : complex array
Laplace transformed solution
M : integer
number of terms (number of values of fp)
alpha: is the real part of the rightmost pole or singularity, which
is chosen based on the desired accuracy (assuming the rightmost
singularity is 0), and tol=10α is the desired tolerance
Returns
-------
result : array
time domain solution for specified times
Reference
---------
de Hoog, F., J. Knight, A. Stokes (1982). An improved method for
numerical inversion of Laplace transforms. SIAM Journal of Scientific
and Statistical Computing 3:357-366, http://dx.doi.org/10.1137/0903022
https://bitbucket.org/klkuhlm/invlap/src/default/invlap.py
"""
# return zeros if all fp equal to zero
if np.all(np.abs(fp) == 0):
return np.zeros(len(t))
NP = 2 * M + 1
scale = 2.0
T = scale * tmax
Nt = len(t)
#
# tol = alpha * 10.0
gamma = alpha - np.log(tol) / (scale * T)
# would it be useful to try re-using
# space between e&q and A&B?
# Kris programmed this as np.complex64
e = np.empty((2 * M + 1, M + 1), dtype=np.complex128)
q = np.empty((2 * M, M), dtype=np.complex128)
d = np.empty(2 * M + 1, dtype=np.complex128)
A = np.empty((2 * M + 2, Nt), dtype=np.complex128)
B = np.empty((2 * M + 2, Nt), dtype=np.complex128)
# initialize Q-D table
e[:, 0] = 0.0
q[0, 0] = fp[1] / (fp[0] / 2.0)
for i in range(1, 2 * M):
q[i, 0] = fp[i + 1] / fp[i]
# rhombus rule for filling triangular Q-D table (e & q)
for r in range(1, M + 1):
# start with e, column 1, 0:2*M-2
mr = 2 * (M - r) + 1
e[0:mr, r] = q[1 : mr + 1, r - 1] - q[0:mr, r - 1] + e[1 : mr + 1, r - 1]
if not r == M:
rq = r + 1
mr = 2 * (M - rq) + 2
for i in range(mr):
q[i, rq - 1] = q[i + 1, rq - 2] * e[i + 1, rq - 1] / e[i, rq - 1]
# build up continued fraction coefficients (d)
d[0] = fp[0] / 2.0
for r in range(1, M + 1):
d[2 * r - 1] = -q[0, r - 1] # even terms
d[2 * r] = -e[0, r] # odd terms
# seed A and B for recurrence
A[0] = 0.0
A[1] = d[0]
B[0:2] = 1.0 + 0j
# base of the power series
z = np.exp(1j * np.pi * t / T)
# coefficients of Pade approximation (A & B)
# using recurrence for all but last term
for i in range(1, 2 * M):
A[i + 1] = A[i] + d[i] * A[i - 1] * z
B[i + 1] = B[i] + d[i] * B[i - 1] * z
# "improved remainder" to continued fraction
brem = (1.0 + (d[2 * M - 1] - d[2 * M]) * z) / 2.0
rem = -brem * (1.0 - np.sqrt(1.0 + d[2 * M] * z / brem**2))
# last term of recurrence using new remainder
A[NP] = A[2 * M] + rem * A[2 * M - 1]
B[NP] = B[2 * M] + rem * B[2 * M - 1]
# diagonal Pade approximation
# F=A/B represents accelerated trapezoid rule
result = np.exp(gamma * t) / T * (A[NP] / B[NP]).real
# print('results:', result)
# print('A', A)
# print('B', B)
return result
[docs]
@numba.njit(nogil=True, cache=True)
def compute_laplace_parameters_numba(tmax, M=20, alpha=1e-10, tol=1e-9):
# 2*M+1 terms in approximation
# desired tolerance (here simply related to alpha)
# tol = alpha * 10.0
nump = 2 * M + 1 # number of terms in approximation
# scaling factor (likely tune-able, but 2 is typical)
scale = 2.0
T = scale * tmax
gamma = alpha - np.log(tol) / (scale * T)
p = gamma + 1j * np.pi * np.arange(nump) / T
return p
[docs]
def invlaptest():
p = compute_laplace_parameters_numba(tmax=10, alpha=1e-10)
fp = 1 / (p + 1) ** 2
t = np.arange(1.0, 10)
ft = invlap(t, 10, fp, 20, alpha=1e-10)
print("approximate from invlap:", ft)
print("exact:", t * np.exp(-t))
[docs]
@numba.njit(nogil=True, cache=True)
def invlapcomp(time, pot, npint, M, tintervals, enumber, etstart, ebc, nlayers):
"""Compute time domain solution for given laplace domain solution.
Parameters
----------
time : array, must be ordered
times for which time domain solution is computed, must start at 0
pot : array of laplace domain solution conform the ttim shape
npint : int
number of p values per interval (=2M + 1)
M : int
order of the approximation
tintervals : time intervals
enumber : array with number of element
etstart : array with starting time of bc in element
ebc : array with boundary condition value of element
nlayers : integer or None (default)
number of layers
Notes
-----
enumber, etstart, and ebc are used because numba cannot deal with a list
of arrays of different lengths (makes sense, actually)
Returns
-------
pot[naq, ntimes] if layers=None,
otherwise pot[len(layers) ,ntimes]
t must be ordered
"""
print_tmin_warning = True # set to False if warning is printed once
print_tmax_warning = True
nelements, naq, npval = pot.shape
nint = len(tintervals) - 1
rv = np.zeros((nlayers, len(time)))
#
# assuming that first time of all bc's is 0
for j in range(len(enumber)):
t = time - etstart[j]
it = 0
if t[0] <= 0: # there are times before start of bc
if t[-1] <= 0: # all times before start of bc, also for len(t)=1
continue
else:
# no effect for any t <= 0
it = np.argmax(t > 0) # find_first
if t[it] < tintervals[0]: # there are times before first interval
if print_tmin_warning:
print("Warning, some of the times are smaller than tmin after")
print("a change in boundary condition. nans are substituted")
print_tmin_warning = False
if t[-1] < tintervals[0]: # all times before first interval
itnew = len(t)
else:
itnew = np.argmax(t >= tintervals[0]) # find_first
rv[:, it:itnew] = np.nan
it = itnew
for n in range(nint):
if n == 0:
tp = t[(t >= tintervals[n]) & (t <= tintervals[n + 1])]
else:
tp = t[(t > tintervals[n]) & (t <= tintervals[n + 1])]
nt = len(tp)
# if nt > 0: # if all zero, don't do the inv transform
if nt == 0:
continue
for i in range(nlayers):
# I used to check the first value only, but got to check if
# none of the values are zero
if not np.any(pot[enumber[j], i, n * npint : (n + 1) * npint] == 0):
rv[i, it : it + nt] += ebc[j] * invlap(
tp,
tintervals[n + 1],
pot[enumber[j], i, n * npint : (n + 1) * npint],
M,
)
it = it + nt
if it < len(t): # there are times above tintervals[-1]
if print_tmax_warning:
print("Warning, some of the times are larger than tmax after")
print("a change in boundary condition. nans are substituted")
print_tmax_warning = False
rv[:, it:] = np.nan
return rv
# The following general function is currently not used but very useful
# to do numerical Laplace transformation
[docs]
@numba.njit(nogil=True, cache=True)
def invlapgen(time, pot, M, tintervals, tstart, ebc):
"""Compute time domain solution for given Laplace domain solution.
Parameters
----------
time : array, must be ordered
times for which time domain solution is computed, must start at 0
pot : 1D array of laplace domain solution of length nint * (2M + 1)
M : int order of the approximation (i.e., (2M + 1) p values per interval)
tintervals : 1D array of time intervals, length nint + 1
tstart : 1D array with starting times of bc in element, length nstart
ebc : 1D array with change in boundary condition value, length nstart
Notes
-----
ebc is the difference with the previous value
Returns
-------
rv[ntimes]
"""
print_tmin_warning = True # set to False if warning is printed once
print_tmax_warning = True
npint = 2 * M + 1
nint = len(tintervals) - 1
rv = np.zeros(len(time))
#
# assuming that first time of all bc's is 0
for j in range(len(tstart)):
t = time - tstart[j]
it = 0
if t[0] <= 0: # there are times before start of bc
if t[-1] <= 0: # all times before start of bc, also for len(t)=1
continue
else:
# no effect for any t <= 0
it = np.argmax(t > 0) # find_first
if t[it] < tintervals[0]: # there are times before first interval
if print_tmin_warning:
print("Warning, some of the times are smaller than tmin after")
print("a change in boundary condition. nans are substituted")
print_tmin_warning = False
if t[-1] < tintervals[0]: # all times before first interval
itnew = len(t)
else:
itnew = np.argmax(t >= tintervals[0]) # find_first
rv[it:itnew] = np.nan
it = itnew
for n in range(nint):
if n == 0:
tp = t[(t >= tintervals[n]) & (t <= tintervals[n + 1])]
else:
tp = t[(t > tintervals[n]) & (t <= tintervals[n + 1])]
nt = len(tp)
# if nt > 0: # if all zero, don't do the inv transform
if nt == 0:
continue
# I used to check the first value only, but got to check if
# none of the values are zero
if not np.any(pot[n * npint : (n + 1) * npint] == 0):
rv[it : it + nt] += ebc[j] * invlap(
tp, tintervals[n + 1], pot[n * npint : (n + 1) * npint], M
)
it = it + nt
if it < len(t): # there are times above tintervals[-1]
if print_tmax_warning:
print("Warning, some of the times are larger than tmax after")
print("a change in boundary condition. nans are substituted")
print_tmax_warning = False
rv[it:] = np.nan
return rv