# Copyright (c) 2012-2023 by the GalSim developers team on GitHub
# https://github.com/GalSim-developers
#
# This file is part of GalSim: The modular galaxy image simulation toolkit.
# https://github.com/GalSim-developers/GalSim
#
# GalSim is free software: redistribution and use in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions, and the disclaimer given in the accompanying LICENSE
# file.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the disclaimer given in the documentation
# and/or other materials provided with the distribution.
#
__all__ = [ 'LookupTable', 'LookupTable2D', '_LookupTable', '_LookupTable2D', 'trapz', ]
import numpy as np
import numbers
import warnings
from . import _galsim
from ._utilities import lazy_property, basestring
from .bounds import BoundsD
from .position import _PositionD
from .errors import *
from .interpolant import convert_interpolant
def _str_array(a):
# Used by both LookupTable.__str__ and LookupTable2D.__str__
# to write the x, f, etc. numpy arrays in an abbreviated form so they don't fill the
# screen with numbers.
# 1. Write the whole array if it has at most 5 values,
# 2. Just write the first two and last two values in the array if longer.
# 3. linewidth defaults to 75, which adds annoying linebreaks here.
# 1000 should be big enough to mean "never".
with np.printoptions(threshold=5, edgeitems=2, linewidth=1000):
return repr(a)
[docs]class LookupTable:
"""
LookupTable represents a lookup table to store function values that may be slow to calculate,
for which interpolating from a lookup table is sufficiently accurate.
A LookupTable may be constructed from two arrays (lists, tuples, or NumPy arrays of
floats/doubles)::
>>> args = [...]
>>> vals = []
>>> for arg in args:
... val = calculateVal(arg)
... vals.append(val)
>>> table = galsim.LookupTable(x=args,f=vals)
Then you can use this table as a replacement for the slow calculation::
>>> other_args = [...]
>>> for arg in other_args:
... val = table(arg)
... [... use val ...]
The default interpolation method is a natural cubic spline. This is usually the best choice,
but we also provide other options, which can be specified by the ``interpolant`` kwarg. The
choices include 'floor', 'ceil', 'linear', 'spline', or a `galsim.Interpolant` object:
- 'floor' takes the value from the previous argument in the table.
- 'ceil' takes the value from the next argument in the table.
- 'nearest' takes the value from the nearest argument in the table.
- 'linear' does linear interpolation between these two values.
- 'spline' uses a cubic spline interpolation, so the interpolated values are smooth at
each argument in the table.
- a `galsim.Interpolant` object or a string convertible to one. This option can be used for
`Lanczos` or `Quintic` interpolation, for example.
Note that specifying the string 'nearest' or 'linear' will use a LookupTable-optimized
interpolant instead of `galsim.Nearest` or `galsim.Linear`, though the latter options can still
be used by passing an `Interpolant` object instead of a string. Also note that to use a
`galsim.Interpolant` in a LookupTable, the input data must be equally spaced, or logarithmically
spaced if ``x_log`` is set to True (see below). Finally, although natural cubic spline used
when interpolant='spline' and the cubic convolution interpolant used when the interpolant
is `galsim.Cubic` both produce piecewise cubic polynomial interpolations, their treatments of
the continuity of derivatives are different (the natural spline is smoother).
There are also two factory functions which can be used to build a LookupTable:
`LookupTable.from_func`
makes a LookupTable from a callable function
`LookupTable.from_file`
reads in a file of x and f values.
The user can also opt to interpolate in log(x) and/or log(f) (if not using a
`galsim.Interpolant`), though this is not the default. It may be a wise choice depending on the
particular function, e.g., for a nearly power-law f(x) (or at least one that is locally
power-law-ish for much of the x range) then it might be a good idea to interpolate in log(x) and
log(f) rather than x and f.
Parameters:
x: The list, tuple, or NumPy array of ``x`` values.
f: The list, tuple, or NumPy array of ``f(x)`` values.
interpolant: Type of interpolation to use, with the options being 'floor', 'ceil',
'nearest', 'linear', 'spline', or a `galsim.Interpolant` or string
convertible to one. [default: 'spline']
x_log: Set to True if you wish to interpolate using log(x) rather than x. Note
that all inputs / outputs will still be x, it's just a question of how the
interpolation is done. [default: False]
f_log: Set to True if you wish to interpolate using log(f) rather than f. Note
that all inputs / outputs will still be f, it's just a question of how the
interpolation is done. [default: False]
"""
def __init__(self, x, f, interpolant='spline', x_log=False, f_log=False):
self.x_log = x_log
self.f_log = f_log
# Check if interpolant is a string that we understand. If not, try convert_interpolant
if interpolant in ('nearest', 'linear', 'ceil', 'floor', 'spline'):
self._interp1d = None
else:
self._interp1d = convert_interpolant(interpolant)
self.interpolant = interpolant
# Sanity checks
if len(x) != len(f):
raise GalSimIncompatibleValuesError("Input array lengths don't match", x=x, f=f)
if len(x) < 2:
raise GalSimValueError("Input arrays too small to interpolate", x)
# turn x and f into numpy arrays so that all subsequent math is possible (unlike for
# lists, tuples). Also make sure the dtype is float
x = np.asarray(x, dtype=float)
if np.all(x[1:] >= x[:-1]):
# Already sorted (a common case, so avoid the sort.
self.x = np.ascontiguousarray(x, dtype=float)
self.f = np.ascontiguousarray(f, dtype=float)
else:
f = np.asarray(f, dtype=float)
s = np.argsort(x)
self.x = np.ascontiguousarray(x[s])
self.f = np.ascontiguousarray(f[s])
self._x_min = self.x[0]
self._x_max = self.x[-1]
if self._x_min == self._x_max:
raise GalSimValueError("All x values are equal", x)
if self.x_log and self.x[0] <= 0.:
raise GalSimValueError("Cannot interpolate in log(x) when table contains x<=0.", x)
if self.f_log and np.any(self.f <= 0.):
raise GalSimValueError("Cannot interpolate in log(f) when table contains f<=0.", f)
# inf causes problems in some cases, so avoid it if used as the maximum x value.
if self._x_max == np.inf:
self.x[-1] = self._x_max = 1.e300
# Check equal-spaced arrays
if self._interp1d is not None:
if self.x_log:
ratio = self.x[1:]/self.x[:-1]
if not np.allclose(ratio, ratio[0]):
raise GalSimIncompatibleValuesError(
"Cannot use a galsim.Interpolant with x_log=True unless log(x) is "
"equally spaced.",
interpolant=interpolant, x_log=x_log, x=x)
else:
dx = np.diff(self.x)
if not np.allclose(dx, dx[0]):
raise GalSimIncompatibleValuesError(
"Cannot use a galsim.Interpolant with x_log=False unless x is "
"equally spaced.",
interpolant=interpolant, x_log=x_log, x=x)
@lazy_property
def _tab(self):
# Store these as attributes, so don't need to worry about C++ layer persisting them.
self._x = np.log(self.x) if self.x_log else self.x
self._f = np.log(self.f) if self.f_log else self.f
_x = self._x.__array_interface__['data'][0]
_f = self._f.__array_interface__['data'][0]
if self._interp1d is not None:
return _galsim.LookupTable(_x, _f, len(self._x), self._interp1d._i)
else:
return _galsim.LookupTable(_x, _f, len(self._x), self.interpolant)
@property
def x_min(self):
"""The minimum x value in the lookup table.
"""
return self._x_min
@property
def x_max(self):
"""The maximum x value in the lookup table.
"""
return self._x_max
def __len__(self): return len(self.x)
[docs] def __call__(self, x):
"""Interpolate the `LookupTable` to get ``f(x)`` at some ``x`` value(s).
When the `LookupTable` object is called with a single argument, it returns the value at that
argument. An exception will be thrown automatically if the ``x`` value is outside the
range of the original tabulated values. The value that is returned is the same type as
that provided as an argument, e.g., if a single value ``x`` is provided then a single value
of ``f`` is returned; if a tuple of ``x`` values is provided then a tuple of ``f`` values
is returned; and so on. Even if interpolation was done using the ``x_log`` option, the
user should still provide ``x`` rather than ``log(x)``.
Parameters:
x: The ``x`` value(s) for which ``f(x)`` should be calculated via interpolation on
the original ``(x,f)`` lookup table. ``x`` can be a single float/double, or a
tuple, list, or arbitrarily shaped 1- or 2-dimensional NumPy array.
Returns:
the interpolated ``f(x)`` value(s).
"""
orig_x = x
# Handle the log(x) if necessary
if self.x_log:
x = np.log(x)
x = np.asarray(x, dtype=float)
try:
if x.shape == ():
f = self._tab.interp(float(x))
else:
dimen = len(x.shape)
if dimen > 1:
f = np.empty_like(x.ravel(), dtype=float)
xx = x.astype(float,copy=False).ravel()
_xx = xx.__array_interface__['data'][0]
_f = f.__array_interface__['data'][0]
self._tab.interpMany(_xx, _f, len(xx))
f = f.reshape(x.shape)
else:
f = np.empty_like(x, dtype=float)
xx = x.astype(float,copy=False)
_xx = xx.__array_interface__['data'][0]
_f = f.__array_interface__['data'][0]
self._tab.interpMany(_xx, _f, len(xx))
except RuntimeError:
# If there were points outside the valid range, this will have raised an exception.
# so call _check_range to give a better error message.
self._check_range(orig_x)
raise # pragma: no cover (shouldn't be able to reach here, but just in case.)
# Handle the log(f) if necessary
if self.f_log:
f = np.exp(f)
return f
[docs] def integrate(self, x_min=None, x_max=None):
r"""Calculate an estimate of the integral of the tabulated function from x_min to x_max:
.. math::
\int_{x_\mathrm{min}}^{x_\mathrm{max}} f(x) dx
This function is not implemented for LookupTables that use log for either x or f,
or that use a ``galsim.Interpolant``. Also, if x_min or x_max are beyond the range
of the tabulated function, the function will be considered to be zero there.
.. note::
The simplest version of this function is equivalent in functionality to the numpy
``trapz`` function. However, it is usually significantly faster. If you have a
time-critical integration for which you are currently using ``np.trapz``::
>>> ans = np.trapz(f, x)
the following replacement may be faster::
>>> ans = galsim.trapz(f, x)
which is an alias for::
>>> ans = galsim._LookupTable(x, f, 'linear').integrate()
Parameters:
x_min: The minimum abscissa to use for the integral. [default: None, which
means to use self.x_min]
x_max: The maximum abscissa to use for the integral. [default: None, which
means to use self.x_max]
Returns:
an estimate of the integral
"""
if self.x_log:
raise GalSimNotImplementedError("log x spacing not implemented yet.")
if self.f_log:
raise GalSimNotImplementedError("log f values not implemented yet.")
if not isinstance(self.interpolant, basestring):
raise GalSimNotImplementedError(
"Integration with interpolant=%s is not implemented."%(self.interpolant))
if x_min is None:
x_min = self.x_min
else:
x_min = max(x_min, self.x_min)
if x_max is None:
x_max = self.x_max
else:
x_max = min(x_max, self.x_max)
if x_min < x_max:
return self._tab.integrate(x_min, x_max)
elif x_min == x_max:
return 0.
else:
return -self.integrate(x_max, x_min)
[docs] def integrate_product(self, g, x_min=None, x_max=None, x_factor=1.):
r"""Calculate an estimate of the integral of the tabulated function multiplied by a second
function from x_min to x_max:
.. math::
\int_{x_\mathrm{min}}^{x_\mathrm{max}} f(x) g(x) dx
If the second function, :math:`g(x)`, is another `LookupTable`, then the quadrature will
use the abscissae from both that function and :math:`f(x)` (i.e. ``self``).
Otherwise, the second function will be evaluated at the abscissae of :math:`f(x)`.
This function is not implemented for LookupTables that use log for either x or f,
or that use a ``galsim.Interpolant``. Also, if x_min or x_max are beyond the range
of either tabulated function, the function will be considered to be zero there.
Also, the second function :math:`g(x)` is always approximated with linear interpolation
between the abscissae, even if it is a `LookupTable` with a different specified
interpolation.
Parameters:
g: The function to multiply by the current function for the integral.
x_min: The minimum abscissa to use for the integral. [default: None, which
means to use self.x_min]
x_max: The maximum abscissa to use for the integral. [default: None, which
means to use self.x_max]
x_factor: Optionally scale the x values of f by this factor when doing the integral.
I.e. Find :math:`\int f(x x_\mathrm{factor}) g(x) dx`. [default: 1]
Returns:
an estimate of the integral
"""
from .utilities import merge_sorted
if self.x_log:
raise GalSimNotImplementedError("log x spacing not implemented yet.")
if self.f_log:
raise GalSimNotImplementedError("log f values not implemented yet.")
if not isinstance(self.interpolant, basestring):
raise GalSimNotImplementedError(
"Integration with interpolant=%s is not implemented."%(self.interpolant))
if x_min is None:
x_min = self.x_min / x_factor
else:
x_min = max(x_min, self.x_min / x_factor)
if x_max is None:
x_max = self.x_max / x_factor
else:
x_max = min(x_max, self.x_max / x_factor)
if x_min > x_max:
return -self.integrate_product(g, x_max, x_min, x_factor)
elif x_min == x_max:
return 0.
if isinstance(g, LookupTable):
x_min = max(x_min, g.x_min)
x_max = min(x_max, g.x_max)
if x_min >= x_max:
return 0.
else:
gx = self.x / x_factor
gx = gx[(gx >= x_min) & (gx <= x_max)]
gx = merge_sorted([gx, [x_min, x_max]])
# Let this raise an appropriate error if g is not a valid function over this domain.
gf = g(gx)
# If g is a constant function (like lambda wave: 1), then this doesn't return
# an array. Make it one.
try:
len(gf)
except TypeError:
gf1 = gf
gf = np.empty_like(gx, dtype=float)
gf.fill(gf1)
g = _LookupTable(gx, gf, 'linear')
return self._tab.integrate_product(g._tab, float(x_min), float(x_max), float(x_factor))
def _check_range(self, x):
slop = (self.x_max - self.x_min) * 1.e-6
if np.min(x,initial=self.x_min) < self.x_min - slop:
xx = np.array(x)
raise GalSimRangeError("x value(s) below the range of the LookupTable.",
xx[xx<self.x_min], self.x_min, self.x_max) from None
if np.max(x,initial=self.x_max) > self.x_max + slop: # pragma: no branch
xx = np.array(x)
raise GalSimRangeError("x value(s) above the range of the LookupTable.",
xx[xx>self.x_max], self.x_min, self.x_max) from None
def getArgs(self):
return self.x
def getVals(self):
return self.f
def getInterp(self):
return self.interpolant
def isLogX(self):
return self.x_log
def isLogF(self):
return self.f_log
def __eq__(self, other):
return (self is other or
(isinstance(other, LookupTable) and
np.array_equal(self.x,other.x) and
np.array_equal(self.f,other.f) and
self.x_log == other.x_log and
self.f_log == other.f_log and
self.interpolant == other.interpolant))
def __ne__(self, other): return not self.__eq__(other)
def __hash__(self):
# Cache this in case self.x, self.f are long.
if not hasattr(self, '_hash'):
self._hash = hash(("galsim.LookupTable", tuple(self.x), tuple(self.f), self.x_log,
self.f_log, self.interpolant))
return self._hash
def __repr__(self):
return 'galsim.LookupTable(x=array(%r), f=array(%r), interpolant=%r, x_log=%r, f_log=%r)'%(
self.x.tolist(), self.f.tolist(), self.interpolant, self.x_log, self.f_log)
def __str__(self):
s = 'galsim.LookupTable(x=%s, f=%s'%(_str_array(self.x), _str_array(self.f))
if self.interpolant != 'spline':
s += ', interpolant=%r'%(self.interpolant)
if self.x_log:
s += ', x_log=True'
if self.f_log:
s += ', f_log=True'
s += ')'
return s
[docs] @classmethod
def from_file(cls, file_name, interpolant='spline', x_log=False, f_log=False, amplitude=1.0):
"""Create a `LookupTable` from a file of x, f values.
This reads in a file, which should contain two columns with the x and f values.
Parameters:
file_name: A file from which to read the ``(x,f)`` pairs.
interpolant: Type of interpolation to use. [default: 'spline']
x_log: Whether the x values should be uniform in log rather than lienar.
[default: False]
f_log: Whether the f values should be interpolated using their logarithms
rather than their raw values. [default: False]
amplitude: An optional scaling of the f values relative to the values in the file
[default: 1.0]
"""
# We don't require pandas as a dependency, but if it's available, this is much faster.
# cf. http://stackoverflow.com/questions/15096269/the-fastest-way-to-read-input-in-python
ParserError = AttributeError # In case we don't get to the line below where we import
# it from pandas.
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import pandas
from pandas.errors import ParserError
data = pandas.read_csv(file_name, comment='#', sep=r'\s+', header=None)
data = data.values.transpose()
except (ImportError, AttributeError, ParserError):
data = np.loadtxt(file_name).transpose()
if data.shape[0] != 2:
raise GalSimValueError("File provided for LookupTable does not have 2 columns",
file_name)
x=data[0]
f=data[1]
if amplitude != 1.0:
f[:] *= amplitude
return LookupTable(x, f, interpolant=interpolant, x_log=x_log, f_log=f_log)
[docs] @classmethod
def from_func(cls, func, x_min, x_max, npoints=2000, interpolant='spline',
x_log=False, f_log=False):
"""Create a `LookupTable` from a callable function
This constructs a `LookupTable` over the given range from x_min and x_max, calculating the
corresponding f values from the given function (technically any callable object).
Parameters:
func: A callable function.
x_min: The minimum x value at which to evalue the function and store in the
lookup table.
x_max: The maximum x value at which to evalue the function and store in the
lookup table.
npoints: Number of x values at which to evaluate the function. [default: 2000]
interpolant: Type of interpolation to use. [default: 'spline']
x_log: Whether the x values should be uniform in log rather than lienar.
[default: False]
f_log: Whether the f values should be interpolated using their logarithms
rather than their raw values. [default: False]
"""
if x_log:
x = np.exp(np.linspace(np.log(x_min), np.log(x_max), npoints))
else:
x = np.linspace(x_min, x_max, npoints)
f = np.array([func(xx) for xx in x], dtype=float)
return cls(x, f, interpolant=interpolant, x_log=x_log, f_log=f_log)
def __getstate__(self):
d = self.__dict__.copy()
d.pop('_tab',None)
return d
def __setstate__(self, d):
self.__dict__ = d
def _LookupTable(x, f, interpolant='spline', x_log=False, f_log=False):
"""Make a `LookupTable` but without using any of the sanity checks or array manipulation used
in the normal initializer.
The input x values must be already sorted.
Parameters:
x: Strictly increasing NumPy array of ``x`` values.
f: NumPy array of ``f(x)`` values.
interpolant: Type of interpolation to use, with the options being 'floor', 'ceil',
'nearest', 'linear', 'spline', or a `galsim.Interpolant` or string
convertible to one. [default: 'spline']
x_log: Set to True if you wish to interpolate using log(x) rather than x. Note
that all inputs / outputs will still be x, it's just a question of how the
interpolation is done. [default: False]
f_log: Set to True if you wish to interpolate using log(f) rather than f. Note
that all inputs / outputs will still be f, it's just a question of how the
interpolation is done. [default: False]
"""
ret = LookupTable.__new__(LookupTable)
ret.x = np.ascontiguousarray(x, dtype=float)
ret.f = np.ascontiguousarray(f, dtype=float)
ret.interpolant = interpolant
ret.x_log = x_log
ret.f_log = f_log
ret._x_min = ret.x[0]
ret._x_max = ret.x[-1]
if interpolant in ('nearest', 'linear', 'ceil', 'floor', 'spline'):
ret._interp1d = None
else:
ret._interp1d = convert_interpolant(interpolant)
return ret
[docs]def trapz(f, x, interpolant='linear'):
"""Integrate f(x) using the trapezoidal rule.
Equivalent to np.trapz(f,x) for 1d array inputs. Intended as a drop-in replacement,
which is usually faster.
Parameters:
f: The ordinates of the function to integrate.
x: The abscissae of the function to integrate.
interpolant: The interpolant to use between the tabulated points. [default: 'linear',
which matches the behavior of np.trapz]
Returns:
Estimate of the integral.
"""
if len(x) >= 2:
return _LookupTable(x, f, interpolant=interpolant).integrate()
else:
return 0.
[docs]class LookupTable2D:
"""
LookupTable2D represents a 2-dimensional lookup table to store function values that may be slow
to calculate, for which interpolating from a lookup table is sufficiently accurate. A
LookupTable2D is also useful for evaluating periodic 2-d functions given samples from a single
period.
A LookupTable2D representing the function f(x, y) may be constructed from a list or array of
``x`` values, a list or array of ``y`` values, and a 2D array of function evaluations at all
combinations of x and y values. For instance::
>>> x = np.arange(5)
>>> y = np.arange(8)
>>> z = x + y[:, np.newaxis] # function is x + y, dimensions of z are (8, 5)
>>> tab2d = galsim.LookupTable2D(x, y, z)
To evaluate new function values with the lookup table, use the () operator::
>>> print tab2d(2.2, 3.3)
5.5
The () operator can also accept sequences (lists, tuples, numpy arrays, ...) for the x and y
arguments at which to evaluate the LookupTable2D. Normally, the x and y sequences should have
the same length, which will also be the length of the output sequence::
>>> print tab2d([1, 2], [3, 5])
array([ 4., 7.])
If you add ``grid=True`` as an additional kwarg, however, then the () operator will generate
interpolated values at the outer product of x-values and y-values. So in this case, the x and
y sequences can have different lengths Nx and Ny, and the result will be a 2D array with
dimensions (Nx, Ny)::
>>> print tab2d([1, 2], [3, 5], grid=True)
array([[ 4., 6.],
[ 5., 7.]])
The default interpolation method is linear. Other choices for the interpolant are:
- 'floor'
- 'ceil'
- 'nearest'
- 'spline' (a Catmull-Rom cubic spline).
- a `galsim.Interpolant` or string convertible to one.
::
>>> tab2d = galsim.LookupTable2D(x, y, z, interpolant='floor')
>>> tab2d(2.2, 3.7)
5.0
>>> tab2d = galsim.LookupTable2D(x, y, z, interpolant='ceil')
>>> tab2d(2.2, 3.7)
7.0
>>> tab2d = galsim.LookupTable2D(x, y, z, interpolant='nearest')
>>> tab2d(2.2, 3.7)
6.0
For interpolant='spline' or a `galsim.Interpolant`, the input arrays must be uniformly spaced.
For interpolant='spline', the derivatives df / dx, df / dy, and d^2 f / dx dy at grid-points may
also optionally be provided if they're known, which will generally yield a more accurate
interpolation (these derivatives will be estimated from finite differences if they're not
provided).
The ``edge_mode`` keyword describes how to handle extrapolation beyond the initial input range.
Possibilities include:
- 'raise': raise an exception. (This is the default.)
- 'warn': issues a warning, then falls back to edge_mode='constant'.
- 'constant': Return a constant specified by the ``constant`` keyword.
- 'wrap': infinitely wrap the initial range in both directions.
In order for LookupTable2D to determine the wrapping period when edge_mode='wrap', either the
x and y grid points need to be equally spaced (in which case the x-period is inferred as
len(x)*(x[1]-x[0]) and similarly for y), or the first/last row/column of f must be identical,
in which case the x-period is inferred as x[-1] - x[0]. (If both conditions are satisfied
(equally-spaced x and y and identical first/last row/column of f, then the x-period is inferred
as len(x)*(x[1]-x[0]))::
>>> x = np.arange(5)
>>> y = np.arange(8)
>>> z = x + y[:, np.newaxis] # function is x + y, dimensions of z are (8, 5)
>>> tab2d = galsim.LookupTable2D(x, y, z, edge_mode='raise')
>>> tab2d(7, 7)
ValueError: Extrapolating beyond input range.
>>> tab2d = galsim.LookupTable2D(x, y, z, edge_mode='constant', constant=1.0)
1.0
>>> tab2d = galsim.LookupTable2D(x, y, z, edge_mode='wrap')
ValueError: Cannot wrap `f` array with unequal first/last column/row.
We extend the x and y arrays with a uniform spacing, though any monotonic spacing would work.
Note that the [(0,1), (0,1)] argument in np.pad below extends the z array by 0 rows/columns in
the leading direction, and 1 row/column in the trailing direction::
>>> x = np.append(x, x[-1] + (x[-1]-x[-2]))
>>> y = np.append(y, y[-1] + (y[-1]-y[-2]))
>>> z = np.pad(z, [(0,1), (0,1)], mode='wrap')
>>> tab2d = galsim.LookupTable2D(x, y, z, edge_mode='wrap')
>>> tab2d(2., 2.)
4.0
>>> tab2d(2.+5, 2.) # The period is 5 in the x direction
4.0
>>> tab2d(2.+3*5, 2.+4*8) # The period is 8 in the y direction
4.0
Parameters:
x: Strictly increasing array of ``x`` positions at which to create table.
y: Strictly increasing array of ``y`` positions at which to create table.
f: Nx by Ny input array of function values.
dfdx: Optional first derivative of f wrt x. Only used if interpolant='spline'.
[default: None]
dfdy: Optional first derivative of f wrt y. Only used if interpolant='spline'.
[default: None]
d2fdxdy: Optional cross derivative of f wrt x and y. Only used if
interpolant='spline'. [default: None]
interpolant: Type of interpolation to use. One of 'floor', 'ceil', 'nearest', 'linear',
'spline', or a `galsim.Interpolant` or string convertible to one.
[default: 'linear']
edge_mode: Keyword controlling how extrapolation beyond the input range is handled.
See above for details. [default: 'raise']
constant: A constant to return when extrapolating beyond the input range and
``edge_mode='constant'``. [default: 0]
"""
def __init__(self, x, y, f, dfdx=None, dfdy=None, d2fdxdy=None,
interpolant='linear', edge_mode='raise', constant=0):
if edge_mode not in ('raise', 'warn', 'wrap', 'constant'):
raise GalSimValueError("Unknown edge_mode.", edge_mode,
('raise', 'warn', 'wrap', 'constant'))
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
f = np.asarray(f, dtype=float)
dx = np.diff(x)
dy = np.diff(y)
equal_spaced = np.allclose(dx, dx[0]) and np.allclose(dy, dy[0])
if not all(dx > 0):
raise GalSimValueError("x input grids is not strictly increasing.", x)
if not all(dy > 0):
raise GalSimValueError("y input grids is not strictly increasing.", y)
fshape = f.shape
if fshape != (len(y), len(x)):
raise GalSimIncompatibleValuesError(
"Shape of f incompatible with lengths of x,y", f=f, x=x, y=y)
# Check if interpolant is a string that we understand. If not, try convert_interpolant
if interpolant in ('nearest', 'linear', 'ceil', 'floor', 'spline'):
self._interp2d = None
padrange = 2 if interpolant == 'spline' else 1
else:
self._interp2d = convert_interpolant(interpolant)
padrange = int(np.ceil(self._interp2d.xrange))
self.interpolant = interpolant
# Check if need equal-spaced arrays
if (self._interp2d is not None or interpolant == 'spline'):
if not equal_spaced:
raise GalSimIncompatibleValuesError(
"Cannot use a galsim.Interpolant in LookupTable2D unless x and y are "
"equally spaced.", interpolant=interpolant, x=x, y=y)
self.edge_mode = edge_mode
self.constant = float(constant)
if self.edge_mode == 'wrap':
# Can wrap if x and y arrays are equally spaced ...
if equal_spaced:
if 2*padrange > len(x) or 2*padrange > len(y):
raise GalSimValueError(
"Cannot wrap an image which is smaller than the Interpolant",
(x, y, interpolant))
# Underlying Table2D requires us to extend x, y, and f.
self.xperiod = x[-1]-x[0]+dx[0]
self.yperiod = y[-1]-y[0]+dy[0]
self.x0 = x[0]
self.y0 = y[0]
x = np.hstack([x[0]-np.cumsum([dx[0]]*(padrange-1)),
x,
x[-1]+np.cumsum([dx[0]]*padrange)])
y = np.hstack([y[0]-np.cumsum([dy[0]]*(padrange-1)),
y,
y[-1]+np.cumsum([dy[0]]*padrange)])
f = np.pad(f, [(padrange-1, padrange)]*2, mode='wrap')
# Can also wrap non-uniform grids if edges match
elif (all(f[0] == f[-1]) and all(f[:,0] == f[:,-1])):
self.x0 = x[0]
self.y0 = y[0]
self.xperiod = x[-1] - x[0]
self.yperiod = y[-1] - y[0]
else:
raise GalSimIncompatibleValuesError(
"Cannot use edge_mode='wrap' unless either x and y are equally "
"spaced or first/last row/column of f are identical.",
edge_mode=edge_mode, x=x, y=y, f=f)
self.x = np.ascontiguousarray(x)
self.y = np.ascontiguousarray(y)
self.f = np.ascontiguousarray(f)
der_exist = [kw is not None for kw in [dfdx, dfdy, d2fdxdy]]
if self.interpolant == 'spline':
if any(der_exist):
if not all(der_exist):
raise GalSimIncompatibleValuesError(
"Must specify all of dfdx, dfdy, d2fdxdy if one is specified",
dfdx=dfdx, dfdy=dfdy, d2fdxdy=d2fdxdy)
else:
# Use finite differences if derivatives not provided
dfdx = np.empty_like(f)
diffx = self.x[2:] - self.x[:-2]
dfdx[:, 1:-1] = (f[:, 2:] - f[:, :-2])/diffx
dfdx[:, 0] = (f[:, 1] - f[:, 0])/dx[0]
dfdx[:, -1] = (f[:, -1] - f[:, -2])/dx[-1]
dfdy = np.empty_like(f)
diffy = self.y[2:] - self.y[:-2]
dfdy[1:-1, :] = (f[2:, :] - f[:-2, :])/diffy[:,None]
dfdy[0, :] = (f[1, :] - f[0, :])/dy[0]
dfdy[-1, :] = (f[-1, :] - f[-2, :])/dy[-1]
d2fdxdy = np.empty_like(f)
d2fdxdy[1:-1, :] = (dfdx[2:, :] - dfdx[:-2, :])/diffy[:,None]
d2fdxdy[0, :] = (dfdx[1, :] - dfdx[0, :])/dy[0]
d2fdxdy[-1, :] = (dfdx[-1, :] - dfdx[-2, :])/dy[-1]
else:
if any(der_exist):
raise GalSimIncompatibleValuesError(
"Only specify dfdx, dfdy, d2fdxdy if interpolant is 'spline'.",
dfdx=dfdx, dfdy=dfdy, d2fdxdy=d2fdxdy, interpolant=interpolant)
if dfdx is not None:
dfdx = np.ascontiguousarray(dfdx, dtype=float)
dfdy = np.ascontiguousarray(dfdy, dtype=float)
d2fdxdy = np.ascontiguousarray(d2fdxdy, dtype=float)
if dfdx.shape != f.shape or dfdy.shape != f.shape or d2fdxdy.shape != f.shape:
raise GalSimIncompatibleValuesError(
"derivative shapes must match f shape",
dfdx=dfdx, dfdy=dfdy, d2fdxdy=d2fdxdy)
self.dfdx = dfdx
self.dfdy = dfdy
self.d2fdxdy = d2fdxdy
@lazy_property
def _tab(self):
_x = self.x.__array_interface__['data'][0]
_y = self.y.__array_interface__['data'][0]
_f = self.f.__array_interface__['data'][0]
if self._interp2d is not None:
return _galsim.LookupTable2D(_x, _y, _f, len(self.x), len(self.y),
self._interp2d._i)
elif self.interpolant == 'spline':
_dfdx = self.dfdx.__array_interface__['data'][0]
_dfdy = self.dfdy.__array_interface__['data'][0]
_d2fdxdy = self.d2fdxdy.__array_interface__['data'][0]
return _galsim.LookupTable2D(_x, _y, _f, len(self.x), len(self.y),
_dfdx, _dfdy, _d2fdxdy)
else:
return _galsim.LookupTable2D(_x, _y, _f, len(self.x), len(self.y),
self.interpolant)
def getXArgs(self):
return self.x
def getYArgs(self):
return self.y
def getVals(self):
return self.f
def _inbounds(self, x, y):
"""Return whether or not *all* coords specified by x and y are in bounds of the original
interpolated array."""
# Only used if edge_mode != 'wrap', so original x/y arrays are unmodified.
return (np.min(x) >= self.x[0] and np.max(x) <= self.x[-1] and
np.min(y) >= self.y[0] and np.max(y) <= self.y[-1])
def _wrap_args(self, x, y):
"""Wrap points back into the fundamental period."""
# Original x and y may have been modified, so need to use x0 and xperiod attributes here.
#x = (x-self.x0) % self.xperiod + self.x0
#y = (y-self.y0) % self.yperiod + self.y0
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
_galsim.WrapArrayToPeriod(_x, len(x), self.x0, self.xperiod)
_galsim.WrapArrayToPeriod(_y, len(y), self.y0, self.yperiod)
return x, y
@property
def _bounds(self):
# Only meaningful if edge_mode is 'raise' or 'warn', in which case original x/y arrays are
# unmodified.
return BoundsD(self.x[0], self.x[-1], self.y[0], self.y[-1])
def _call_inbounds(self, x, y, grid=False):
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
if grid:
f = np.empty((len(y), len(x)), dtype=float)
_f = f.__array_interface__['data'][0]
self._tab.interpGrid(_x, _y, _f, len(x), len(y))
return f
else:
f = np.empty_like(x, dtype=float)
_f = f.__array_interface__['data'][0]
self._tab.interpMany(_x, _y, _f, len(x))
return f
def _call_constant(self, x, y, grid=False):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
if grid:
f = np.empty((len(y), len(x)), dtype=float)
# Fill in interpolated values first, then go back and fill in
# constants
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
_f = f.__array_interface__['data'][0]
self._tab.interpGrid(_x, _y, _f, len(x), len(y))
badx = (x < self.x[0]) | (x > self.x[-1])
bady = (y < self.y[0]) | (y > self.y[-1])
f[bady, :] = self.constant
f[:, badx] = self.constant
return f
else:
# Start with constant array, then interpolate good positions
f = np.empty_like(x, dtype=float)
f.fill(self.constant)
good = ((x >= self.x[0]) & (x <= self.x[-1]) &
(y >= self.y[0]) & (y <= self.y[-1]))
xx = np.ascontiguousarray(x[good].ravel(), dtype=float)
yy = np.ascontiguousarray(y[good].ravel(), dtype=float)
tmp = np.empty_like(xx, dtype=float)
_xx = xx.__array_interface__['data'][0]
_yy = yy.__array_interface__['data'][0]
_tmp = tmp.__array_interface__['data'][0]
self._tab.interpMany(_xx, _yy, _tmp, len(xx))
f[good] = tmp
return f
def _call_raise(self, x, y, grid=False):
if not self._inbounds(x, y):
pos = find_out_of_bounds_position(x, y, self._bounds, grid)
raise GalSimBoundsError("Extrapolating beyond input range.",
pos, self._bounds)
return self._call_inbounds(x, y, grid)
def _call_warn(self, x, y, grid=False):
if not self._inbounds(x, y):
pos = find_out_of_bounds_position(x, y, self._bounds, grid)
galsim_warn("Extrapolating beyond input range. {!r} not in {!r}".format(
pos, self._bounds))
return self._call_constant(x, y, grid)
def _call_wrap(self, x, y, grid=False):
x, y = self._wrap_args(x, y)
return self._call_inbounds(x, y, grid)
[docs] def __call__(self, x, y, grid=False):
"""Interpolate at an arbitrary point or points.
Parameters:
x: Either a single x value or an array of x values at which to interpolate.
y: Either a single y value or an array of y values at which to interpolate.
grid: Optional boolean indicating that output should be a 2D array corresponding
to the outer product of input values. If False (default), then the output
array will be congruent to x and y.
Returns:
a scalar value if x and y are scalar, or a numpy array if x and y are arrays.
"""
if np.__version__ >= "2.0":
# I'm not sure if there is a simpler way to do this in 2.0.
# We want a copy when edge_mode == wrap. Otherwise, only copy if dtype changes or
# x,y aren't already arrays. That used to be simple...
copy = True if self.edge_mode=='wrap' else None
else:
copy = self.edge_mode=='wrap'
x1 = np.array(x, dtype=float, copy=copy)
y1 = np.array(y, dtype=float, copy=copy)
x2 = np.ascontiguousarray(x1.ravel(), dtype=float)
y2 = np.ascontiguousarray(y1.ravel(), dtype=float)
if self.edge_mode == 'raise':
f = self._call_raise(x2, y2, grid)
elif self.edge_mode == 'warn':
f = self._call_warn(x2, y2, grid)
elif self.edge_mode == 'wrap':
f = self._call_wrap(x2, y2, grid)
else: # constant
f = self._call_constant(x2, y2, grid)
if isinstance(x, numbers.Real):
return f[0]
else:
if not grid:
f = f.reshape(x1.shape)
return f
def _gradient_inbounds(self, x, y, grid=False):
if grid:
dfdx = np.empty((len(y), len(x)), dtype=float)
dfdy = np.empty((len(y), len(x)), dtype=float)
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
_dfdx = dfdx.__array_interface__['data'][0]
_dfdy = dfdy.__array_interface__['data'][0]
self._tab.gradientGrid(_x, _y, _dfdx, _dfdy, len(x), len(y))
return dfdx, dfdy
else:
dfdx = np.empty_like(x)
dfdy = np.empty_like(x)
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
_dfdx = dfdx.__array_interface__['data'][0]
_dfdy = dfdy.__array_interface__['data'][0]
self._tab.gradientMany(_x, _y, _dfdx, _dfdy, len(x))
return dfdx, dfdy
def _gradient_raise(self, x, y, grid=False):
if not self._inbounds(x, y):
pos = find_out_of_bounds_position(x, y, self._bounds, grid)
raise GalSimBoundsError("Extrapolating beyond input range.",
pos, self._bounds)
return self._gradient_inbounds(x, y, grid)
def _gradient_warn(self, x, y, grid=False):
if not self._inbounds(x, y):
pos = find_out_of_bounds_position(x, y, self._bounds, grid)
galsim_warn("Extrapolating beyond input range. {!r} not in {!r}".format(
pos, self._bounds))
return self._gradient_constant(x, y, grid)
def _gradient_wrap(self, x, y, grid=False):
x, y = self._wrap_args(x, y)
return self._gradient_inbounds(x, y, grid)
def _gradient_constant(self, x, y, grid=False):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
if grid:
dfdx = np.empty((len(y), len(x)), dtype=float)
dfdy = np.empty((len(y), len(x)), dtype=float)
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
_dfdx = dfdx.__array_interface__['data'][0]
_dfdy = dfdy.__array_interface__['data'][0]
self._tab.gradientGrid(_x, _y, _dfdx, _dfdy, len(x), len(y))
badx = (x < self.x[0]) | (x > self.x[-1])
bady = (y < self.y[0]) | (y > self.y[-1])
dfdx[bady,:] = 0.0
dfdx[:, badx] = 0.0
dfdy[bady,:] = 0.0
dfdy[:, badx] = 0.0
return dfdx, dfdy
else:
dfdx = np.empty_like(x, dtype=float)
dfdy = np.empty_like(x, dtype=float)
dfdx.fill(0.0)
dfdy.fill(0.0)
good = ((x >= self.x[0]) & (x <= self.x[-1]) &
(y >= self.y[0]) & (y <= self.y[-1]))
x = np.ascontiguousarray(x[good].ravel(), dtype=float)
y = np.ascontiguousarray(y[good].ravel(), dtype=float)
tmp1 = np.empty_like(x, dtype=float)
tmp2 = np.empty_like(x, dtype=float)
_x = x.__array_interface__['data'][0]
_y = y.__array_interface__['data'][0]
_tmp1 = tmp1.__array_interface__['data'][0]
_tmp2 = tmp2.__array_interface__['data'][0]
self._tab.gradientMany(_x, _y, _tmp1, _tmp2, len(x))
dfdx[good] = tmp1
dfdy[good] = tmp2
return dfdx, dfdy
[docs] def gradient(self, x, y, grid=False):
"""Calculate the gradient of the function at an arbitrary point or points.
Parameters:
x: Either a single x value or an array of x values at which to compute
the gradient.
y: Either a single y value or an array of y values at which to compute
the gradient.
grid: Optional boolean indicating that output should be a 2-tuple of 2D arrays
corresponding to the outer product of input values. If False (default),
then the output arrays will be congruent to x and y.
Returns:
A tuple of (dfdx, dfdy) where dfdx, dfdy are single values (if x,y were single
values) or numpy arrays.
"""
if np.__version__ >= "2.0":
copy = True if self.edge_mode=='wrap' else None
else:
copy = self.edge_mode=='wrap'
x1 = np.array(x, dtype=float, copy=copy)
y1 = np.array(y, dtype=float, copy=copy)
x2 = np.ascontiguousarray(x1.ravel(), dtype=float)
y2 = np.ascontiguousarray(y1.ravel(), dtype=float)
if self.edge_mode == 'raise':
dfdx, dfdy = self._gradient_raise(x2, y2, grid)
if self.edge_mode == 'warn':
dfdx, dfdy = self._gradient_warn(x2, y2, grid)
elif self.edge_mode == 'wrap':
dfdx, dfdy = self._gradient_wrap(x2, y2, grid)
else: # constant
dfdx, dfdy = self._gradient_constant(x2, y2, grid)
if isinstance(x, numbers.Real):
return dfdx[0], dfdy[0]
else:
if not grid:
dfdx = dfdx.reshape(x1.shape)
dfdy = dfdy.reshape(x1.shape)
return dfdx, dfdy
def __str__(self):
return "galsim.LookupTable2D(x=%s, y=%s, f=[%s,...,%s], interpolant=%r, edge_mode=%r)"%(
_str_array(self.x), _str_array(self.y),
_str_array(self.f[0]), _str_array(self.f[-1]),
self.interpolant, self.edge_mode)
def __repr__(self):
return ("galsim.LookupTable2D(x=array(%r), y=array(%r), "
"f=array(%r), interpolant=%r, edge_mode=%r, constant=%r)"%(
self.x.tolist(), self.y.tolist(), self.f.tolist(), self.interpolant, self.edge_mode,
self.constant))
def __eq__(self, other):
if self is other: return True
if not (isinstance(other, LookupTable2D) and
np.array_equal(self.x,other.x) and
np.array_equal(self.y,other.y) and
np.array_equal(self.f,other.f) and
self.interpolant == other.interpolant and
self.edge_mode == other.edge_mode and
self.constant == other.constant):
return False
else:
if self.interpolant == 'spline':
return (np.array_equal(self.dfdx, other.dfdx) and
np.array_equal(self.dfdy, other.dfdy) and
np.array_equal(self.d2fdxdy, other.d2fdxdy))
return True
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
if not hasattr(self, '_hash'):
self._hash = hash(("galsim.LookupTable2D", tuple(self.x.ravel()), tuple(self.y.ravel()),
tuple(self.f.ravel()), self.interpolant, self.edge_mode,
self.constant,
tuple(self.dfdx.ravel()) if self.dfdx is not None else None,
tuple(self.dfdy.ravel()) if self.dfdy is not None else None,
tuple(self.d2fdxdy.ravel()) if self.d2fdxdy is not None else None))
return self._hash
def __getstate__(self):
d = self.__dict__.copy()
d.pop('_tab',None)
return d
def __setstate__(self, d):
self.__dict__ = d
[docs]def _LookupTable2D(x, y, f, interpolant, edge_mode, constant,
dfdx=None, dfdy=None, d2fdxdy=None,
x0=None, y0=None, xperiod=None, yperiod=None):
"""Make a `LookupTable2D` but without using any of the sanity checks or array manipulation used
in the normal initializer.
"""
ret = LookupTable2D.__new__(LookupTable2D)
ret.x = x
ret.y = y
ret.f = f
ret.interpolant = interpolant
ret.edge_mode = edge_mode
ret.constant = constant
ret.dfdx = dfdx
ret.dfdy = dfdy
ret.d2fdxdy = d2fdxdy
ret.x0 = x0
ret.y0 = y0
ret.xperiod = xperiod
ret.yperiod = yperiod
if interpolant in ('nearest', 'linear', 'ceil', 'floor', 'spline'):
ret._interp2d = None
else:
ret._interp2d = convert_interpolant(interpolant)
return ret
[docs]def find_out_of_bounds_position(x, y, bounds, grid=False):
"""Given arrays of x and y values that are known to contain at least one
position that is out-of-bounds of the given bounds instance, return one
such PositionD.
Parameters:
x: Array of x values
y: Array of y values
bounds: `Bounds` instance
grid: Bool indicating whether to check the outer product of x and y
(grid=True), or each sequential pair of x and y (grid=False).
If the latter, then x and y should have the same shape.
Returns:
a `PositionD` from x and y that is out-of-bounds of bounds.
"""
if grid:
# It's enough to check corners for grid input
for x_ in (np.min(x), np.max(x)):
for y_ in (np.min(y), np.max(y)):
if (x_ < bounds.xmin or x_ > bounds.xmax or
y_ < bounds.ymin or y_ > bounds.ymax):
return _PositionD(x_, y_)
else:
# Faster to check all points than to iterate through them one-by-one?
w = np.where((x < bounds.xmin) | (x > bounds.xmax) |
(y < bounds.ymin) | (y > bounds.ymax))
if len(w[0]) > 0:
return _PositionD(x[w[0][0]], y[w[0][0]])
raise GalSimError("No out-of-bounds position")