Source code for galsim.bandpass

# 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__ = [ 'Bandpass' ]

import numpy as np
import os
from astropy import units
from numbers import Real

from .errors import GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError
from .table import LookupTable, _LookupTable
from ._utilities import WeakMethod, basestring
from . import integ
from . import meta_data
from . import utilities
from .sed import SED

[docs]class Bandpass: """Simple bandpass object, which models the transmission fraction of incident light as a function of wavelength, for either an entire optical path (e.g., atmosphere, reflecting and refracting optics, filters, CCD quantum efficiency), or some intermediate piece thereof. Bandpasses representing individual components may be combined through the ``*`` operator to form a new Bandpass object representing the composite optical path. Bandpasses are callable, returning dimensionless throughput as a function of wavelength in nm. Bandpasses are immutable; all transformative methods return *new* Bandpasses, and leave their originating Bandpasses unaltered. Bandpasses require ``blue_limit`` and ``red_limit`` attributes, which may either be explicitly set at initialization, or are inferred from the initializing `LookupTable` or 2-column file. Outside of the wavelength interval between ``blue_limit`` and ``red_limit``, the throughput is returned as zero, regardless of the ``throughput`` input parameter. Bandpasses may be multiplied by other Bandpasses, functions, scalars, or `SED` instances. The product of a Bandpass with an `SED` is a new `SED`. The Bandpass effective wavelength is stored in the python property ``effective_wavelength``. We use throughput-weighted average wavelength (which is independent of any `SED`) as our definition for effective wavelength. For Bandpasses defined using a `LookupTable`, a numpy.array of wavelengths, ``wave_list``, defining the table is maintained. Bandpasses defined as products of two other Bandpasses will define their ``wave_list`` as the union of multiplicand ``wave_list`` values, although limited to the range between the new product ``blue_limit`` and ``red_limit``. (This implementation detail may affect the choice of integrator used to draw a `ChromaticObject`.) The input parameter, throughput, may be one of several possible forms: 1. a regular python function (or an object that acts like a function) 2. a `LookupTable` 3. a file from which a `LookupTable` can be read in 4. a string which can be evaluated to a function of ``wave`` via ``eval('lambda wave : '+throughput)``, e.g.:: throughput = '0.8 + 0.2 * (wave-800)' The argument of ``throughput`` will be the wavelength in units specified by ``wave_type``. (See below.) The output should be the dimensionless throughput at that wavelength. (Note we use ``wave`` rather than ``lambda``, since ``lambda`` is a python reserved word.) The argument ``wave_type`` specifies the units to assume for wavelength and must be one of 'nm', 'nanometer', 'nanometers', 'A', 'Ang', 'Angstrom', or 'Angstroms', or an astropy distance unit. (For the string values, case is unimportant.) If given, blue_limit and red_limit are taken to be in these units as well. Note that the ``wave_type`` parameter does not propagate into other methods of Bandpass. For instance, `Bandpass.__call__` assumes its input argument is in nanometers. Finally, a Bandpass may have zeropoint attribute, which is a float used to convert flux (in photons/s/cm^2) to magnitudes:: mag = -2.5*log10(flux) + zeropoint You can either set the zeropoint at initialization, or via the `withZeropoint` method. Note that the zeropoint attribute does not propagate if you get a new Bandpass by multiplying or dividing an old Bandpass. Parameters: throughput: Function defining the throughput at each wavelength. See above for valid options for this parameter. wave_type: The units to use for the wavelength argument of the ``throughput`` function. See above for details. blue_limit: Hard cut off of bandpass on the blue side. [default: None, but required if throughput is not a `LookupTable` or file. See above.] red_limit: Hard cut off of bandpass on the red side. [default: None, but required if throughput is not a `LookupTable` or file. See above.] zeropoint: Set the zero-point for this Bandpass. Here, this can only be a float value. See the method `withZeropoint` for other options for how to set this using a particular spectrum (AB, Vega, etc.) [default: None] interpolant: If reading from a file, what interpolant to use. [default: 'linear'] """ def __init__(self, throughput, wave_type, blue_limit=None, red_limit=None, zeropoint=None, interpolant='linear', _wave_list=None, _tp=None): # Note that `_wave_list` acts as a private construction variable that overrides the way that # `wave_list` is normally constructed (see `Bandpass.__mul__` below) self._orig_tp = throughput # Save this for pickling. self._tp = _tp # This will normally become orig_tp turned into an actual # function (see _initialize_tp()), although in some cases, # it can be supplied directly as a constructor argument. if blue_limit is not None and red_limit is not None and blue_limit >= red_limit: raise GalSimRangeError("blue_limit must be less than red_limit", blue_limit, None, red_limit) self.blue_limit = blue_limit # These may change as we go through this. self.red_limit = red_limit self.zeropoint = zeropoint self.interpolant = interpolant # Parse the various options for wave_type if isinstance(wave_type, str): if wave_type.lower() in ('nm', 'nanometer', 'nanometers'): self.wave_type = 'nm' self.wave_factor = 1. elif wave_type.lower() in ('a', 'ang', 'angstrom', 'angstroms'): self.wave_type = 'Angstrom' self.wave_factor = 10. else: raise GalSimValueError("Invalid wave_type.", wave_type, ('nm', 'Angstrom')) else: self.wave_type = wave_type try: self.wave_factor = (1*units.nm).to(self.wave_type).value if self.wave_factor == 1.: self.wave_type = 'nm' elif abs(self.wave_factor-10.) < 2.e-15: # This doesn't come out exactly 10. self.wave_type = 'Angstrom' self.wave_factor = 10. except units.UnitConversionError: # Unlike in SED, we require a distance unit for wave_type raise GalSimValueError("Invalid wave_type. Must be a distance.", wave_type) # Convert string input into a real function (possibly a LookupTable) self._initialize_tp() if _wave_list is not None: # Manual override! Be careful! self.wave_list = _wave_list # This also means that red_limit and blue_limit are already set correctly. # Don't change them. #assert self.blue_limit is not None #assert self.red_limit is not None self._setup_func() return # Account for wave_factor in wavelength limits if self.wave_factor != 1.0: if self.blue_limit is not None: self.blue_limit /= self.wave_factor if self.red_limit is not None: self.red_limit /= self.wave_factor # Assign blue and red limits of bandpass if isinstance(self._tp, LookupTable): if self.blue_limit is None: self.blue_limit = float(self._tp.x_min)/self.wave_factor if self.red_limit is None: self.red_limit = float(self._tp.x_max)/self.wave_factor else: if self.blue_limit is None or self.red_limit is None: raise GalSimIncompatibleValuesError( "red_limit and blue_limit are required if throughput is not a LookupTable.", blue_limit=blue_limit, red_limit=red_limit, throughput=throughput) # Sanity check blue/red limit and create self.wave_list if isinstance(self._tp, LookupTable): self.wave_list = np.array(self._tp.getArgs())/self.wave_factor # Make sure that blue_limit and red_limit are within LookupTable region of support. if self.blue_limit < (self._tp.x_min/self.wave_factor): raise GalSimRangeError("Cannot set blue_limit to be less than throughput x_min", self.blue_limit, self._tp.x_min, self._tp.x_max) if self.red_limit > (self._tp.x_max/self.wave_factor): raise GalSimRangeError("Cannot set red_limit to be greater than throughput x_max", self.red_limit, self._tp.x_min, self._tp.x_max) # Remove any values that are outside the limits self.wave_list = self.wave_list[np.logical_and(self.wave_list >= self.blue_limit, self.wave_list <= self.red_limit) ] # Make sure that blue_limit and red_limit are part of wave_list. if self.red_limit not in self.wave_list: np.append(self.wave_list, self.red_limit) if self.blue_limit not in self.wave_list: np.insert(self.wave_list, 0, self.blue_limit) else: self.wave_list = np.array([], dtype=float) self._setup_func() def _setup_func(self): if self.wave_factor == 1.: self.func = WeakMethod(self._func_trivial) else: self.func = WeakMethod(self._func_factor) def _func_trivial(self, wave): return self._tp(np.asarray(wave,dtype=float)) def _func_factor(self, wave): return self._tp(np.asarray(wave) * self.wave_factor) def _initialize_tp(self): # Turn the input tp into a real function self._tp. # The function cannot be pickled, so will need to do this in setstate as well as init. if self._tp is not None: pass elif isinstance(self._orig_tp, basestring): isfile, filename = utilities.check_share_file(self._orig_tp, 'bandpasses') if isfile: self._tp = LookupTable.from_file(filename, interpolant=self.interpolant) else: if self.blue_limit is None or self.red_limit is None: raise GalSimIncompatibleValuesError( "red_limit and blue_limit are required if throughput is not a LookupTable.", blue_limit=None, red_limit=None, throughput=self._orig_tp) test_wave = self.blue_limit try: self._tp = utilities.math_eval('lambda wave : ' + self._orig_tp) test_value = self._tp(test_wave) except Exception as e: raise GalSimValueError( "String throughput must either be a valid filename or something that " "can eval to a function of wave.\n Caught error: %s."%(e), self._orig_tp) if not isinstance(test_value, Real): raise GalSimValueError( "The given throughput function did not return a valid " "number at test wavelength %s: got %s."%(test_wave, test_value), self._orig_tp) else: self._tp = self._orig_tp def __mul__(self, other): # Watch out for 4 types of `other`: # 1. SED: delegate to SED.__mul__(bandpass) # 2. Bandpass: return a Bandpass, but carefully propagate blue/red limit and wave_list. # 3. Callable: return a Bandpass # 4. Scalar: return a Bandpass # Delegate SED * Bandpass to SED.__mul__: if isinstance(other, SED): return other.__mul__(self) # Bandpass * Bandpass -> Bandpass if isinstance(other, Bandpass): wave_list, blue_limit, red_limit = utilities.combine_wave_list([self, other]) tp = lambda w: self(w) * other(w) return Bandpass(tp, 'nm', blue_limit=blue_limit, red_limit=red_limit, zeropoint=None, _wave_list=wave_list) # Product of Bandpass with generic callable or scalar is a rescaled Bandpass. wave_type = 'nm' if hasattr(other, '__call__'): tp = lambda w: self.func(w) * other(w) elif isinstance(self._tp, LookupTable): # If other is not a function, then there is no loss of accuracy by applying the # factor directly to the LookupTable, if that's what we are using. # Make sure to keep the same properties about the table, wave_type. wave_type = self.wave_type x = self._tp.getArgs() f = [ val * other for val in self._tp.getVals() ] tp = LookupTable(x, f, x_log=self._tp.x_log, f_log=self._tp.f_log, interpolant=self._tp.interpolant) else: tp = lambda w: self.func(w) * other return Bandpass(tp, wave_type, self.blue_limit, self.red_limit, _wave_list=self.wave_list) def __rmul__(self, other): return self*other # Doesn't check for divide by zero, so be careful. def __div__(self, other): # Watch out for 4 types of `other`: # 1. SED: prohibit. # 2. Bandpass: return a Bandpass, but carefully propagate blue/red limit and wave_list. # 3. Callable: return a Bandpass # 4. Scalar: return a Bandpass if isinstance(other, SED): raise TypeError("Cannot divide Bandpass by SED.") # Bandpass / Bandpass -> Bandpass if isinstance(other, Bandpass): wave_list, blue_limit, red_limit = utilities.combine_wave_list([self, other]) tp = lambda w: self(w) / other(w) return Bandpass(tp, 'nm', blue_limit=blue_limit, red_limit=red_limit, zeropoint=None, _wave_list=wave_list) # Quotient of Bandpass with generic callable or scalar is a rescaled Bandpass. wave_type = 'nm' if hasattr(other, '__call__'): tp = lambda w: self.func(w) / other(w) elif isinstance(self._tp, LookupTable): # If other is not a function, then there is no loss of accuracy by applying the # factor directly to the LookupTable, if that's what we are using. # Make sure to keep the same properties about the table, wave_type. wave_type = self.wave_type x = self._tp.getArgs() f = [ val / other for val in self._tp.getVals() ] tp = LookupTable(x, f, x_log=self._tp.x_log, f_log=self._tp.f_log, interpolant=self._tp.interpolant) else: tp = lambda w: self.func(w) / other return Bandpass(tp, wave_type, self.blue_limit, self.red_limit, _wave_list=self.wave_list) __truediv__ = __div__
[docs] def __call__(self, wave): """Return dimensionless throughput of bandpass at given wavelength in nanometers. Note that outside of the wavelength range defined by the ``blue_limit`` and ``red_limit`` attributes, the throughput is assumed to be zero. Parameters: wave: Wavelength in nanometers. (Either a scalar or a numpy array) Returns: the dimensionless throughput. """ wave = np.asarray(wave) if wave.shape == (): if (wave >= self.blue_limit and wave <= self.red_limit): return self.func(float(wave)) else: return 0.0 else: wgood = (wave >= self.blue_limit) & (wave <= self.red_limit) ret = np.zeros(wave.shape, dtype=float) np.place(ret, wgood, self.func(wave[wgood])) return ret
@property def effective_wavelength(self): """The effective wavelength of the `Bandpass`. An alias for ``self.calculateEffectiveWavelength()``. """ return self.calculateEffectiveWavelength()
[docs] def calculateEffectiveWavelength(self, precise=False): """Calculate, store, and return the effective wavelength for this bandpass. We define the effective wavelength as the throughput-weighted average wavelength, which is SED-independent. Units are nanometers. Parameters: precise: Optionally use a more precise integration method when the bandpass uses a `LookupTable` rather than the normal trapezoid rule. [default: False] """ if not hasattr(self, '_effective_wavelength') or precise: if len(self.wave_list) > 0 and not precise: num = self._tp.integrate_product(lambda w:w, self.blue_limit, self.red_limit, x_factor=self.wave_factor) denom = self._tp.integrate(self.blue_limit*self.wave_factor, self.red_limit*self.wave_factor) / self.wave_factor else: num = integ.int1d(lambda w: self.func(w) * w, self.blue_limit, self.red_limit) denom = integ.int1d(self.func, self.blue_limit, self.red_limit) self._effective_wavelength = num / denom return self._effective_wavelength
[docs] def withZeropoint(self, zeropoint): """Assign a zeropoint to this `Bandpass`. A bandpass zeropoint is a float used to convert flux (in photons/s/cm^2) to magnitudes:: mag = -2.5*log10(flux) + zeropoint Note that the zeropoint attribute does not propagate if you get a new `Bandpass` by multiplying or dividing an old `Bandpass`. The ``zeropoint`` argument can take a variety of possible forms: 1. a number, which will be the zeropoint 2. a `galsim.SED`. In this case, the zeropoint is set such that the magnitude of the supplied `SED` through the `Bandpass` is 0.0 3. the string 'AB'. In this case, use an AB zeropoint. 4. the string 'Vega'. Use a Vega zeropoint. 5. the string 'ST'. Use a HST STmag zeropoint. Parameters: zeropoint: See above for valid input options Returns: new `Bandpass` with zeropoint set. """ # Convert `zeropoint` from str to galsim.SED. if isinstance(zeropoint, str): if zeropoint.upper()=='AB': AB_source = 3631e-23 # 3631 Jy in units of erg/s/Hz/cm^2 sed = SED(lambda wave: AB_source, wave_type='nm', flux_type='fnu') elif zeropoint.upper()=='ST': # Use HST STmags: http://www.stsci.edu/hst/acs/analysis/zeropoints ST_flambda = 3.63e-8 # erg/s/cm^2/nm sed = SED(lambda wave: ST_flambda, wave_type='nm', flux_type='flambda') elif zeropoint.upper()=='VEGA': # Use vega spectrum for SED vegafile = os.path.join(meta_data.share_dir, "SEDs", "vega.txt") sed = SED(vegafile, wave_type='nm', flux_type='flambda') else: raise GalSimValueError("Unrecognized Zeropoint string.", zeropoint, ('AB', 'ST', 'VEGA')) zeropoint = sed # Convert `zeropoint` from galsim.SED to float if isinstance(zeropoint, SED): flux = zeropoint.calculateFlux(self) zeropoint = 2.5 * np.log10(flux) # Should be a float now (or maybe an int). If not, raise an exception. if not isinstance(zeropoint, (float, int)): raise TypeError( "Don't know how to handle zeropoint of type: {0}".format(type(zeropoint))) return Bandpass(self._orig_tp, self.wave_type, self.blue_limit, self.red_limit, zeropoint=zeropoint, interpolant=self.interpolant, _wave_list=self.wave_list, _tp=self._tp)
[docs] def truncate(self, blue_limit=None, red_limit=None, relative_throughput=None, preserve_zp='auto'): """Return a bandpass with its wavelength range truncated. This function truncate the range of the bandpass either explicitly (with ``blue_limit`` or ``red_limit`` or both) or automatically, just trimming off leading and trailing wavelength ranges where the relative throughput is less than some amount (``relative_throughput``). This second option using relative_throughput is only available for bandpasses initialized with a `LookupTable` or from a file, not when using a regular python function or a string evaluation. This function does not remove any intermediate wavelength ranges, but see thin() for a method that can thin out the intermediate values. When truncating a bandpass that already has an assigned zeropoint, there are several possibilities for what should happen to the new (returned) bandpass by default. If red and/or blue limits are given, then the new bandpass will have no assigned zeropoint because it is difficult to predict what should happen if the bandpass is being arbitrarily truncated. If ``relative_throughput`` is given, often corresponding to low-level truncation that results in little change in observed quantities, then the new bandpass is assigned the same zeropoint as the original. This default behavior is called 'auto'. The user can also give boolean True or False values. Parameters: blue_limit: Truncate blue side of bandpass at this wavelength in nm. [default: None] red_limit: Truncate red side of bandpass at this wavelength in nm. [default: None] relative_throughput: Truncate leading or trailing wavelengths that are below this relative throughput level. (See above for details.) Either ``blue_limit`` and/or ``red_limit`` should be supplied, or ``relative_throughput`` should be supplied -- but ``relative_throughput`` should not be combined with one of the limits. [default: None] preserve_zp: If True, the new truncated `Bandpass` will be assigned the same zeropoint as the original. If False, the new truncated `Bandpass` will have a zeropoint of None. If 'auto', the new truncated `Bandpass` will have the same zeropoint as the original when truncating using ``relative_throughput``, but will have a zeropoint of None when truncating using 'blue_limit' and/or 'red_limit'. [default: 'auto'] Returns: the truncated `Bandpass`. """ # Enforce the choice of a single mode of truncation. if relative_throughput is not None: if blue_limit is not None or red_limit is not None: raise GalSimIncompatibleValuesError( "Truncate using relative_throughput or red/blue_limit, not both!", blue_limit=blue_limit, red_limit=red_limit, relative_throughput=relative_throughput) if preserve_zp == 'auto': if relative_throughput is not None: preserve_zp = True else: preserve_zp = False # Check for weird input if preserve_zp is not True and preserve_zp is not False: raise GalSimValueError("Unrecognized input for preserve_zp.",preserve_zp) if blue_limit is None: blue_limit = self.blue_limit else: if blue_limit < self.blue_limit: raise GalSimRangeError("Supplied blue_limit may not be bluer than the original.", blue_limit, self.blue_limit, self.red_limit) if red_limit is None: red_limit = self.red_limit else: if red_limit > self.red_limit: raise GalSimRangeError("Supplied red_limit may not be redder than the original.", red_limit, self.blue_limit, self.red_limit) wave_list = self.wave_list if len(self.wave_list) > 0: wave = np.array(self.wave_list) tp = self.func(wave) if relative_throughput is not None: w = (tp >= tp.max()*relative_throughput).nonzero() blue_limit = max([np.min(wave[w]), blue_limit]) red_limit = min([np.max(wave[w]), red_limit]) wave_list = wave_list[np.logical_and(wave_list >= blue_limit, wave_list <= red_limit) ] elif relative_throughput is not None: raise GalSimIncompatibleValuesError( "Can only truncate with relative_throughput argument if throughput is " "a LookupTable", relative_throughput=relative_throughput, throughput=self._orig_tp) if preserve_zp: return Bandpass(self._orig_tp, self.wave_type, blue_limit, red_limit, zeropoint=self.zeropoint, interpolant=self.interpolant, _wave_list=wave_list, _tp=self._tp) else: return Bandpass(self._orig_tp, self.wave_type, blue_limit, red_limit, interpolant=self.interpolant, _wave_list=wave_list, _tp=self._tp)
[docs] def thin(self, rel_err=1.e-4, trim_zeros=True, preserve_range=True, fast_search=True, preserve_zp=True): """Thin out the internal wavelengths of a `Bandpass` that uses a `LookupTable`. If the bandpass was initialized with a `LookupTable` or from a file (which internally creates a `LookupTable`), this function removes tabulated values while keeping the integral over the set of tabulated values still accurate to the given relative error. That is, the integral of the bandpass function is preserved to a relative precision of ``rel_err``, while eliminating as many internal wavelength values as possible. This process will usually help speed up integrations using this bandpass. You should weigh the speed improvements against your fidelity requirements for your particular use case. By default, this routine will preserve the zeropoint of the original bandpass by assigning it to the new thinned bandpass. The justification for this choice is that when using an AB zeropoint, a typical optical bandpass, and the default thinning ``rel_err`` value, the zeropoint for the new and thinned bandpasses changes by 10^-6. However, if you are thinning a lot, and/or want to do extremely precise tests, you can set ``preserve_zp=False`` and then recalculate the zeropoint after thinning. Parameters: rel_err: The relative error allowed in the integral over the throughput function. [default: 1.e-4] trim_zeros: Remove redundant leading and trailing points where f=0? (The last leading point with f=0 and the first trailing point with f=0 will be retained). Note that if both trim_leading_zeros and preserve_range are True, then the only the range of ``x`` *after* zero trimming is preserved. [default: True] preserve_range: Should the original range (``blue_limit`` and ``red_limit``) of the `Bandpass` be preserved? (True) Or should the ends be trimmed to include only the region where the integral is significant? (False) [default: True] fast_search: If set to True, then the underlying algorithm will use a relatively fast O(N) algorithm to select points to include in the thinned approximation. If set to False, then a slower O(N^2) algorithm will be used. We have found that the slower algorithm tends to yield a thinned representation that retains fewer samples while still meeting the relative error requirement, and may also be somewhat more robust when computing an `SED` flux through a `Bandpass` when a significant fraction of the integrated flux passes through low throughput bandpass light leaks. [default: True] preserve_zp: If True, the new thinned `Bandpass` will be assigned the same zeropoint as the original. If False, the new thinned `Bandpass` will have a zeropoint of None. [default: True] Returns: the thinned `Bandpass`. """ if len(self.wave_list) > 0: x = self.wave_list f = self(x) interpolant = (self.interpolant if not isinstance(self._tp, LookupTable) else self._tp.interpolant) newx, newf = utilities.thin_tabulated_values(x, f, rel_err=rel_err, trim_zeros=trim_zeros, preserve_range=preserve_range, fast_search=fast_search, interpolant=interpolant) tp = _LookupTable(newx, newf, interpolant) blue_limit = np.min(newx) red_limit = np.max(newx) wave_list = np.array(newx) if preserve_zp: return Bandpass(tp, 'nm', blue_limit, red_limit, _wave_list=wave_list, zeropoint=self.zeropoint) else: return Bandpass(tp, 'nm', blue_limit, red_limit, _wave_list=wave_list) else: return self
def __eq__(self, other): return (self is other or (isinstance(other, Bandpass) and self._orig_tp == other._orig_tp and self.blue_limit == other.blue_limit and self.red_limit == other.red_limit and self.wave_factor == other.wave_factor and self.zeropoint == other.zeropoint and np.array_equal(self.wave_list, other.wave_list))) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): # Cache this in case self._orig_tp or self.wave_list is long. if not hasattr(self, '_hash'): self._hash = hash(("galsim.Bandpass", self._orig_tp, self.blue_limit, self.red_limit, self.wave_factor, self.zeropoint, tuple(self.wave_list))) return self._hash def __repr__(self): return ('galsim.Bandpass(%r, wave_type=%r, blue_limit=%r, red_limit=%r, zeropoint=%r, ' 'interpolant=%r, _wave_list=array(%r))')%( self._orig_tp, self.wave_type, self.blue_limit, self.red_limit, self.zeropoint, self.interpolant, self.wave_list.tolist()) def __str__(self): orig_tp = repr(self._orig_tp) if len(orig_tp) > 80: orig_tp = str(self._orig_tp) return 'galsim.Bandpass(%s)'%self._orig_tp def __getstate__(self): d = self.__dict__.copy() if not isinstance(d['_tp'], LookupTable): del d['_tp'] del d['func'] return d def __setstate__(self, d): self.__dict__ = d if '_tp' not in d: self._tp = None # If _tp is already set, this is will just set func. self._initialize_tp() self._setup_func()