From 5d13e35dfc6776cfdc6da6011b3d9d2d1bb64dc0 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Wed, 24 Jul 2024 07:58:20 -0700 Subject: [PATCH 01/15] Start adding Quantity to config layer --- galsim/config/bandpass.py | 13 ++++++- galsim/config/value.py | 24 +++++++++++- galsim/config/value_eval.py | 4 +- tests/test_config_image.py | 5 ++- tests/test_config_value.py | 73 +++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/galsim/config/bandpass.py b/galsim/config/bandpass.py index 5d7ff3e6b0..4e16506fe7 100644 --- a/galsim/config/bandpass.py +++ b/galsim/config/bandpass.py @@ -17,6 +17,7 @@ # import logging +from astropy.units import Quantity from .util import LoggerWrapper from .value import ParseValue, GetAllParams, GetIndex @@ -132,8 +133,16 @@ def buildBandpass(self, config, base, logger): """ logger = LoggerWrapper(logger) - req = {'file_name': str, 'wave_type': str} - opt = {'thin' : float, 'blue_limit' : float, 'red_limit' : float, 'zeropoint': float } + req = { + 'file_name': str, + 'wave_type': str + } + opt = { + 'thin' : float, + 'blue_limit' : (float, Quantity), + 'red_limit' : (float, Quantity), + 'zeropoint': (float, str) + } kwargs, safe = GetAllParams(config, base, req=req, opt=opt) diff --git a/galsim/config/value.py b/galsim/config/value.py index 9742a676cd..b2f906631a 100644 --- a/galsim/config/value.py +++ b/galsim/config/value.py @@ -16,6 +16,7 @@ # and/or other materials provided with the distribution. # +from astropy.units import Quantity from .util import PropagateIndexKeyRNGNum, GetIndex, ParseExtendedKey from ..errors import GalSimConfigError, GalSimConfigValueError @@ -61,6 +62,16 @@ def ParseValue(config, key, base, value_type): """ from .gsobject import BuildGSObject + if isinstance(value_type, tuple): + for vt in value_type: + try: + return ParseValue(config, key, base, vt) + except GalSimConfigError: + pass + else: + raise GalSimConfigError( + "No valid value_type found for %s"%key, value_type) + # Special: if the "value_type" is GSObject, then switch over to that builder instead. if value_type is GSObject: return BuildGSObject(config, key, base) @@ -777,6 +788,14 @@ def _GenerateFromCurrent(config, base, value_type): raise GalSimConfigError("%s\nError generating Current value with key = %s"%(e,k)) +def _GenerateFromQuantity(config, base, value_type): + """Return a Quantity from a value and a unit + """ + req = { 'value' : float, 'unit' : str } + kwargs, safe = GetAllParams(config, base, req=req) + return Quantity(kwargs['value'], kwargs['unit']), safe + + def RegisterValueType(type_name, gen_func, valid_types, input_type=None): """Register a value type for use by the config apparatus. @@ -825,8 +844,8 @@ def RegisterValueType(type_name, gen_func, valid_types, input_type=None): [ float, int, bool, str, Angle, Shear, PositionD, CelestialCoord, LookupTable ]) RegisterValueType('Current', _GenerateFromCurrent, [ float, int, bool, str, Angle, Shear, PositionD, CelestialCoord, LookupTable, - dict, list, None ]) -RegisterValueType('Sum', _GenerateFromSum, [ float, int, Angle, Shear, PositionD ]) + dict, list, None, Quantity ]) +RegisterValueType('Sum', _GenerateFromSum, [ float, int, Angle, Shear, PositionD, Quantity ]) RegisterValueType('Sequence', _GenerateFromSequence, [ float, int, bool ]) RegisterValueType('NumberedFile', _GenerateFromNumberedFile, [ str ]) RegisterValueType('FormattedStr', _GenerateFromFormattedStr, [ str ]) @@ -845,3 +864,4 @@ def RegisterValueType(type_name, gen_func, valid_types, input_type=None): RegisterValueType('RTheta', _GenerateFromRTheta, [ PositionD ]) RegisterValueType('RADec', _GenerateFromRADec, [ CelestialCoord ]) RegisterValueType('File', _GenerateFromFile, [ LookupTable ]) +RegisterValueType('Quantity', _GenerateFromQuantity, [ Quantity ]) diff --git a/galsim/config/value_eval.py b/galsim/config/value_eval.py index 9294b34daa..6d11801b0d 100644 --- a/galsim/config/value_eval.py +++ b/galsim/config/value_eval.py @@ -17,6 +17,7 @@ # import numpy as np import re +from astropy.units import Quantity from .util import PropagateIndexKeyRNGNum from .value import GetCurrentValue, GetAllParams, RegisterValueType @@ -94,6 +95,7 @@ def _GenerateFromEval(config, base, value_type): exec('import numpy', gdict) exec('import numpy as np', gdict) exec('import os', gdict) + exec('import astropy.units as u', gdict) ImportModules(base, gdict) base['_eval_gdict'] = gdict else: @@ -195,4 +197,4 @@ def _GenerateFromEval(config, base, value_type): # Register this as a valid value type RegisterValueType('Eval', _GenerateFromEval, [ float, int, bool, str, Angle, Shear, PositionD, CelestialCoord, - LookupTable, dict, list, None ]) + LookupTable, dict, list, None, Quantity ]) diff --git a/tests/test_config_image.py b/tests/test_config_image.py index 5115120bca..765fa0dd67 100644 --- a/tests/test_config_image.py +++ b/tests/test_config_image.py @@ -23,6 +23,7 @@ import math import re import warnings +import astropy.units as u import galsim from galsim_test_helpers import * @@ -1982,8 +1983,8 @@ def test_bandpass(): 'file_name' : 'ACS_wfc_F814W.dat', 'wave_type' : 'nm', 'thin' : [1.e-4, 1.e-5, 1.e-6], - 'blue_limit': 700, - 'red_limit': 950, + 'blue_limit': 7000*u.Angstrom, # Try mismatched units + 'red_limit': 9500*u.Angstrom, }, 'bp3' : galsim.Bandpass('LSST_g.dat', 'nm'), diff --git a/tests/test_config_value.py b/tests/test_config_value.py index f2c4e0db81..658e04d4e0 100644 --- a/tests/test_config_value.py +++ b/tests/test_config_value.py @@ -1922,6 +1922,79 @@ def test_eval(): np.testing.assert_almost_equal(ps_mu, mu) +def test_quantity(): + import astropy.units as u + config = { + 'length': 1.0 * u.m, + 'length2': '1.0 m', + 'length3': '100.0 cm', + 'length4': { + 'type': 'Quantity', + 'value': 0.001, + 'unit': 'km', + }, + 'length5': { + 'type': 'Quantity', + 'value': 0.001, + 'unit': u.km, + }, + 'length6': '$1.0 * u.m', + 'length7': '10 kg', # Not a length! + 'length8': { + 'type': 'Quantity', + 'value': { + 'type': 'Random', + 'min': 0.0, + 'max': 1.0, + }, + 'unit': 'm', + }, + 'length9': { + 'type': 'Sum', + 'items': [ + { + 'type': 'Quantity', + 'value': 1.0, + 'unit': 'm', + }, + { + 'type': 'Quantity', + 'value': 1.0, + 'unit': 'cm', + } + ] + }, + 'length10': { + 'type': 'Current', + 'key': 'length', + }, + } + + value, _ = galsim.config.ParseValue(config, 'length', config, u.Quantity) + assert value == 1.0 * u.m + value, _ = galsim.config.ParseValue(config, 'length2', config, u.Quantity) + assert value == 1.0 * u.m + value, _ = galsim.config.ParseValue(config, 'length3', config, u.Quantity) + assert value == 1.0 * u.m + value, _ = galsim.config.ParseValue(config, 'length4', config, u.Quantity) + assert value == 1.0 * u.m + value, _ = galsim.config.ParseValue(config, 'length5', config, u.Quantity) + assert value == 1.0 * u.m + value, _ = galsim.config.ParseValue(config, 'length6', config, u.Quantity) + assert value == 1.0 * u.m + # We can demand a Quantity, but there's currently no way to demand a + # particular dimensionality. So this one just yields a mass. + value, _ = galsim.config.ParseValue(config, 'length7', config, u.Quantity) + assert value == 10 * u.kg + value, _ = galsim.config.ParseValue(config, 'length8', config, u.Quantity) + assert 0 <= value.value <= 1.0 + assert value.unit == u.m + value, _ = galsim.config.ParseValue(config, 'length9', config, u.Quantity) + assert value == 1.01 * u.m + value, _ = galsim.config.ParseValue(config, 'length10', config, u.Quantity) + assert value == 1.0 * u.m + + if __name__ == "__main__": testfns = [v for k, v in vars().items() if k[:5] == 'test_' and callable(v)] runtests(testfns) From 5918b331f63b3a93ecbd7d7c581ce88537d2e0e4 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Fri, 13 Sep 2024 12:18:41 -0700 Subject: [PATCH 02/15] Enable Quantity for bandpass red and blue limits. --- galsim/bandpass.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/galsim/bandpass.py b/galsim/bandpass.py index 3d40c5f42a..c93e11a517 100644 --- a/galsim/bandpass.py +++ b/galsim/bandpass.py @@ -79,8 +79,9 @@ class Bandpass: 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. + distance unit. (For the string values, case is unimportant.) If given as floats, blue_limit + and red_limit are taken to be in these units as well. (If given as an astropy Quantity, then + the units are taken directly and converted to ``wave_type``.) 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. @@ -118,14 +119,6 @@ def __init__(self, throughput, wave_type, blue_limit=None, red_limit=None, # 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'): @@ -149,6 +142,19 @@ def __init__(self, throughput, wave_type, blue_limit=None, red_limit=None, # Unlike in SED, we require a distance unit for wave_type raise GalSimValueError("Invalid wave_type. Must be a distance.", wave_type) + if isinstance(blue_limit, units.Quantity): + blue_limit = blue_limit.to_value(units.Unit(self.wave_type)) + if isinstance(red_limit, units.Quantity): + red_limit = red_limit.to_value(units.Unit(self.wave_type)) + + 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 + # Convert string input into a real function (possibly a LookupTable) self._initialize_tp() From 642cddea6ab64b47d02411c83c85d21c89476632 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Fri, 13 Sep 2024 12:53:24 -0700 Subject: [PATCH 03/15] Add astropy.units.Unit as config value --- galsim/config/value.py | 10 +++++++++- galsim/config/value_eval.py | 4 ++-- tests/test_config_value.py | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/galsim/config/value.py b/galsim/config/value.py index b2f906631a..919774dac2 100644 --- a/galsim/config/value.py +++ b/galsim/config/value.py @@ -16,7 +16,7 @@ # and/or other materials provided with the distribution. # -from astropy.units import Quantity +from astropy.units import Quantity, Unit from .util import PropagateIndexKeyRNGNum, GetIndex, ParseExtendedKey from ..errors import GalSimConfigError, GalSimConfigValueError @@ -796,6 +796,13 @@ def _GenerateFromQuantity(config, base, value_type): return Quantity(kwargs['value'], kwargs['unit']), safe +def _GenerateFromUnit(config, base, value_type): + """Return a Unit from a string + """ + req = { 'unit' : str } + kwargs, safe = GetAllParams(config, base, req=req) + return Unit(kwargs['unit']), safe + def RegisterValueType(type_name, gen_func, valid_types, input_type=None): """Register a value type for use by the config apparatus. @@ -865,3 +872,4 @@ def RegisterValueType(type_name, gen_func, valid_types, input_type=None): RegisterValueType('RADec', _GenerateFromRADec, [ CelestialCoord ]) RegisterValueType('File', _GenerateFromFile, [ LookupTable ]) RegisterValueType('Quantity', _GenerateFromQuantity, [ Quantity ]) +RegisterValueType('Unit', _GenerateFromUnit, [ Unit ]) diff --git a/galsim/config/value_eval.py b/galsim/config/value_eval.py index 6d11801b0d..43d762dd81 100644 --- a/galsim/config/value_eval.py +++ b/galsim/config/value_eval.py @@ -17,7 +17,7 @@ # import numpy as np import re -from astropy.units import Quantity +from astropy.units import Quantity, Unit from .util import PropagateIndexKeyRNGNum from .value import GetCurrentValue, GetAllParams, RegisterValueType @@ -197,4 +197,4 @@ def _GenerateFromEval(config, base, value_type): # Register this as a valid value type RegisterValueType('Eval', _GenerateFromEval, [ float, int, bool, str, Angle, Shear, PositionD, CelestialCoord, - LookupTable, dict, list, None, Quantity ]) + LookupTable, dict, list, None, Quantity, Unit ]) diff --git a/tests/test_config_value.py b/tests/test_config_value.py index 658e04d4e0..0078150caf 100644 --- a/tests/test_config_value.py +++ b/tests/test_config_value.py @@ -1995,6 +1995,30 @@ def test_quantity(): assert value == 1.0 * u.m +def test_astropy_unit(): + import astropy.units as u + config = { + 'mass1': u.kg, + 'mass2': 'kg', + 'mass3': '$u.kg', + 'mass4': { + 'type': 'Unit', + 'unit': 'kg', + }, + 'area1': 'm^2', + 'area2': '$u.m * u.m', + 'area3': '$u.m**2' + } + + for k in ['mass1', 'mass2', 'mass3', 'mass4']: + value, _ = galsim.config.ParseValue(config, k, config, u.Unit) + assert value == u.kg + + for k in ['area1', 'area2', 'area3']: + value, _ = galsim.config.ParseValue(config, k, config, u.Unit) + assert value == u.m**2 + + if __name__ == "__main__": testfns = [v for k, v in vars().items() if k[:5] == 'test_' and callable(v)] runtests(testfns) From 0324fc5ba11532fb35fcf3fee5c78fe78e6a1300 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Fri, 13 Sep 2024 12:56:17 -0700 Subject: [PATCH 04/15] Enable bandpass wave_type to be astropy unit in config --- galsim/config/bandpass.py | 4 ++-- tests/test_config_image.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/galsim/config/bandpass.py b/galsim/config/bandpass.py index 4e16506fe7..9ff71cb331 100644 --- a/galsim/config/bandpass.py +++ b/galsim/config/bandpass.py @@ -17,7 +17,7 @@ # import logging -from astropy.units import Quantity +from astropy.units import Quantity, Unit from .util import LoggerWrapper from .value import ParseValue, GetAllParams, GetIndex @@ -135,7 +135,7 @@ def buildBandpass(self, config, base, logger): req = { 'file_name': str, - 'wave_type': str + 'wave_type': (str, Unit), } opt = { 'thin' : float, diff --git a/tests/test_config_image.py b/tests/test_config_image.py index 765fa0dd67..505903d206 100644 --- a/tests/test_config_image.py +++ b/tests/test_config_image.py @@ -1981,7 +1981,7 @@ def test_bandpass(): 'bp2' : { 'type' : 'FileBandpass', 'file_name' : 'ACS_wfc_F814W.dat', - 'wave_type' : 'nm', + 'wave_type' : u.nm, 'thin' : [1.e-4, 1.e-5, 1.e-6], 'blue_limit': 7000*u.Angstrom, # Try mismatched units 'red_limit': 9500*u.Angstrom, From 76c5cbdc25bd9233dcc730ac8cf2a3927ec08c54 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Fri, 13 Sep 2024 13:04:27 -0700 Subject: [PATCH 05/15] Enable sed wave/flux type as astropy.units.Unit in config --- galsim/config/sed.py | 8 +++++++- tests/test_config_image.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/galsim/config/sed.py b/galsim/config/sed.py index 8128c120d5..021247a3b3 100644 --- a/galsim/config/sed.py +++ b/galsim/config/sed.py @@ -16,6 +16,8 @@ # and/or other materials provided with the distribution. # +from astropy.units import Quantity, Unit + from .util import LoggerWrapper from .value import ParseValue, GetAllParams, GetIndex from .input import RegisterInputConnectedType @@ -131,7 +133,11 @@ def buildSED(self, config, base, logger): """ logger = LoggerWrapper(logger) - req = {'file_name': str, 'wave_type': str, 'flux_type': str} + req = { + 'file_name': str, + 'wave_type': (Unit, str), + 'flux_type': (Unit, str), + } opt = {'norm_flux_density': float, 'norm_wavelength': float, 'norm_flux': float, 'redshift': float} ignore = ['norm_bandpass'] diff --git a/tests/test_config_image.py b/tests/test_config_image.py index 505903d206..08476ead99 100644 --- a/tests/test_config_image.py +++ b/tests/test_config_image.py @@ -2844,8 +2844,8 @@ def test_chromatic(): 'sed': { 'file_name': 'CWW_E_ext.sed', - 'wave_type': 'Ang', - 'flux_type': 'flambda', + 'wave_type': u.Angstrom, + 'flux_type': u.erg/u.Angstrom/u.cm**2/u.s, 'norm_flux_density': 1.0, 'norm_wavelength': 500, 'redshift': 0.8, From b5ffdf728e43f94afa4316493c30b1d40ded7f9d Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Fri, 13 Sep 2024 14:36:46 -0700 Subject: [PATCH 06/15] Can construct bandpass with Vega zeropoint --- galsim/config/bandpass.py | 3 +++ tests/test_config_image.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/galsim/config/bandpass.py b/galsim/config/bandpass.py index 9ff71cb331..2c6ef278b8 100644 --- a/galsim/config/bandpass.py +++ b/galsim/config/bandpass.py @@ -148,9 +148,12 @@ def buildBandpass(self, config, base, logger): file_name = kwargs.pop('file_name') thin = kwargs.pop('thin', None) + zeropoint = kwargs.pop('zeropoint', None) logger.info("Reading Bandpass file: %s",file_name) bandpass = Bandpass(file_name, **kwargs) + if zeropoint: + bandpass = bandpass.withZeropoint(zeropoint) if thin: bandpass = bandpass.thin(thin) diff --git a/tests/test_config_image.py b/tests/test_config_image.py index 08476ead99..6aaadc343f 100644 --- a/tests/test_config_image.py +++ b/tests/test_config_image.py @@ -2000,6 +2000,12 @@ def test_bandpass(): 'str' : '@bp1 * @bp3' }, + 'bpz' : { + 'file_name' : 'chromatic_reference_images/simple_bandpass.dat', + 'wave_type' : 'nm', + 'zeropoint' : 'Vega', + }, + 'bad1' : 34, 'bad2' : { 'type' : 'Invalid' }, } @@ -2042,6 +2048,9 @@ def test_bandpass(): assert bp9 is not bp2 assert bp9 == bp2b.thin(1.e-5) + bpz = galsim.config.BuildBandpass(config, 'bpz', config)[0] + assert bpz == bp1.withZeropoint('Vega') + for bad in ['bad1', 'bad2']: with assert_raises(galsim.GalSimConfigError): galsim.config.BuildBandpass(config, bad, config) From 771d45bb2539f96abccdb6ca1eb1a336db4a9a99 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Fri, 13 Sep 2024 15:08:05 -0700 Subject: [PATCH 07/15] Let SED config normalization by quantiful --- galsim/config/sed.py | 10 +++++++--- tests/test_config_image.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/galsim/config/sed.py b/galsim/config/sed.py index 021247a3b3..05ed926ddf 100644 --- a/galsim/config/sed.py +++ b/galsim/config/sed.py @@ -138,8 +138,12 @@ def buildSED(self, config, base, logger): 'wave_type': (Unit, str), 'flux_type': (Unit, str), } - opt = {'norm_flux_density': float, 'norm_wavelength': float, - 'norm_flux': float, 'redshift': float} + opt = { + 'norm_flux_density': (float, Quantity), + 'norm_wavelength': (float, Quantity), + 'norm_flux': float, + 'redshift': float + } ignore = ['norm_bandpass'] kwargs, safe = GetAllParams(config, base, req=req, opt=opt, ignore=ignore) @@ -155,7 +159,7 @@ def buildSED(self, config, base, logger): logger.info("Using SED file: %s",file_name) sed = read_sed_file(file_name, wave_type, flux_type) - if norm_flux_density: + if norm_flux_density is not None: sed = sed.withFluxDensity(norm_flux_density, wavelength=norm_wavelength) elif norm_flux: bandpass, safe1 = BuildBandpass(config, 'norm_bandpass', base, logger) diff --git a/tests/test_config_image.py b/tests/test_config_image.py index 6aaadc343f..bf68d51f62 100644 --- a/tests/test_config_image.py +++ b/tests/test_config_image.py @@ -3289,8 +3289,8 @@ def test_sensor(): 'file_name': 'CWW_E_ext.sed', 'wave_type': 'Ang', 'flux_type': 'flambda', - 'norm_flux_density': 1.0, - 'norm_wavelength': 500, + 'norm_flux_density': 1.0*u.erg/u.s/u.cm**2/u.nm, + 'norm_wavelength': 500*u.nm, 'redshift' : '@gal.redshift', }, }, From 99c8391632399179a4233592a0aae45796fa621e Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 10:28:58 -0700 Subject: [PATCH 08/15] Add Quantity support to DCR config layer --- galsim/config/value.py | 2 +- galsim/photon_array.py | 24 ++++++++++++++++++++---- tests/test_photon_array.py | 7 ++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/galsim/config/value.py b/galsim/config/value.py index 919774dac2..ce26f92e2c 100644 --- a/galsim/config/value.py +++ b/galsim/config/value.py @@ -66,7 +66,7 @@ def ParseValue(config, key, base, value_type): for vt in value_type: try: return ParseValue(config, key, base, vt) - except GalSimConfigError: + except (GalSimConfigError, TypeError): pass else: raise GalSimConfigError( diff --git a/galsim/photon_array.py b/galsim/photon_array.py index 48f1bcbbc9..e1edd9c11c 100644 --- a/galsim/photon_array.py +++ b/galsim/photon_array.py @@ -16,12 +16,13 @@ # and/or other materials provided with the distribution. # -__all__ = [ 'PhotonArray', 'PhotonOp', 'WavelengthSampler', 'FRatioAngles', +__all__ = [ 'PhotonArray', 'PhotonOp', 'WavelengthSampler', 'FRatioAngles', 'PhotonDCR', 'Refraction', 'FocusDepth', 'PupilImageSampler', 'PupilAnnulusSampler', 'TimeSampler', 'ScaleFlux', 'ScaleWavelength' ] import numpy as np +import astropy.units as u from . import _galsim from .random import BaseDeviate @@ -1017,9 +1018,14 @@ class PhotonDCR(PhotonOp): H2O_pressure: Water vapor pressure in kiloPascals. [default: 1.067 kPa] """ _req_params = { 'base_wavelength' : float } - _opt_params = { 'scale_unit' : str, 'alpha' : float, - 'parallactic_angle' : Angle, 'latitude' : Angle, - 'pressure' : float, 'temperature' : float, 'H2O_pressure' : float } + _opt_params = { 'scale_unit' : str, + 'alpha' : float, + 'parallactic_angle' : Angle, + 'latitude' : Angle, + 'pressure' : (float, u.Quantity), + 'temperature' : (float, u.Quantity), + 'H2O_pressure' : (float, u.Quantity) + } _single_params = [ { 'zenith_angle' : Angle, 'HA' : Angle, 'zenith_coord' : CelestialCoord } ] def __init__(self, base_wavelength, scale_unit=arcsec, **kwargs): @@ -1032,6 +1038,16 @@ def __init__(self, base_wavelength, scale_unit=arcsec, **kwargs): self.alpha = kwargs.pop('alpha', 0.) self.zenith_angle, self.parallactic_angle, self.kw = dcr.parse_dcr_angles(**kwargs) + # Convert any weather data to the appropriate units + p = self.kw.get('pressure', None) + if p is not None and isinstance(p, u.Quantity): + self.kw['pressure'] = p.to_value(u.kPa) + t = self.kw.get('temperature', None) + if t is not None and isinstance(t, u.Quantity): + self.kw['temperature'] = t.to_value(u.K) + h = self.kw.get('H2O_pressure', None) + if h is not None and isinstance(h, u.Quantity): + self.kw['H2O_pressure'] = h.to_value(u.kPa) # Any remaining kwargs will get forwarded to galsim.dcr.get_refraction # Check that they're valid diff --git a/tests/test_photon_array.py b/tests/test_photon_array.py index f17cf409ea..fd7bd5fa5b 100644 --- a/tests/test_photon_array.py +++ b/tests/test_photon_array.py @@ -18,6 +18,7 @@ import unittest import numpy as np +import astropy.units as u import os import warnings @@ -841,9 +842,9 @@ def test_dcr(): 'base_wavelength': base_wavelength, 'HA': local_sidereal_time-obj_coord.ra, 'latitude': '-30:14:23.76 deg', - 'pressure': 72, - 'temperature': 290, - 'H2O_pressure': 0.9, + 'pressure': 72*u.kPa, + 'temperature': 290, #'290 K', + 'H2O_pressure': '$900*u.Pa', } im5c = galsim.config.BuildImage(config) assert im5c == im5 From 26b74b3a71ed21b3d53a971ab990763e3cf369a0 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 11:35:21 -0700 Subject: [PATCH 09/15] Add lam/diam/r0 Quantity support to gsobjects --- galsim/airy.py | 15 ++++- galsim/kolmogorov.py | 23 +++++++- galsim/phase_psf.py | 9 ++- galsim/second_kick.py | 15 ++++- galsim/vonkarman.py | 24 +++++++- tests/test_config_gsobject.py | 100 +++++++++++++++++++++++++++++++++- 6 files changed, 172 insertions(+), 14 deletions(-) diff --git a/galsim/airy.py b/galsim/airy.py index ff61d3e150..301b406edf 100644 --- a/galsim/airy.py +++ b/galsim/airy.py @@ -19,6 +19,7 @@ __all__ = [ 'Airy' ] import math +import astropy.units as u from . import _galsim from .gsobject import GSObject @@ -80,13 +81,16 @@ class Airy(GSObject): gsparams: An optional `GSParams` argument. [default: None] """ _req_params = { } - _opt_params = { "flux" : float , "obscuration" : float, "diam" : float, - "scale_unit" : str } + _opt_params = { "flux" : float , + "obscuration" : float, + "diam" : (float, u.Quantity), + "scale_unit" : str + } # Note that this is not quite right; it's true that either lam_over_diam or lam should be # supplied, but if lam is supplied then diam is required. Errors in which parameters are used # may be caught either by config or by the python code itself, depending on the particular # error. - _single_params = [{ "lam_over_diam" : float , "lam" : float } ] + _single_params = [{ "lam_over_diam" : float , "lam" : (float, u.Quantity) } ] # For an unobscured Airy, we have the following factor which can be derived using the # integral result given in the Wikipedia page (http://en.wikipedia.org/wiki/Airy_disk), @@ -108,6 +112,11 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, obscuration=0., flux self._flux = float(flux) self._gsparams = GSParams.check(gsparams) + if isinstance(lam, u.Quantity): + lam = lam.to_value(u.nm) + if isinstance(diam, u.Quantity): + diam = diam.to_value(u.m) + # Parse arguments: either lam_over_diam in arbitrary units, or lam in nm and diam in m. # If the latter, then get lam_over_diam in units of scale_unit, as specified in # docstring. diff --git a/galsim/kolmogorov.py b/galsim/kolmogorov.py index 5b7813cbfc..615a542a64 100644 --- a/galsim/kolmogorov.py +++ b/galsim/kolmogorov.py @@ -19,6 +19,7 @@ __all__ = [ 'Kolmogorov' ] import numpy as np +import astropy.units as u import math from . import _galsim @@ -120,12 +121,21 @@ class Kolmogorov(GSObject): fwhm: The full-width half-max size half_light_radius: The half-light radius """ - _opt_params = { "flux" : float, "r0" : float, "r0_500" : float, "scale_unit" : str } + _opt_params = { + "flux" : float, + "r0" : (float, u.Quantity), + "r0_500" : (float, u.Quantity), + "scale_unit" : str + } # Note that this is not quite right; it's true that exactly one of these 4 should be supplied, # but if lam is supplied then r0 is required. Errors in which parameters are used may be # caught either by config or by the python code itself, depending on the particular error. - _single_params = [ { "lam_over_r0" : float, "fwhm" : float, "half_light_radius" : float, - "lam" : float } ] + _single_params = [ { + "lam_over_r0" : float, + "fwhm" : float, + "half_light_radius" : float, + "lam" : (float, u.Quantity) + } ] # The FWHM of the Kolmogorov PSF is ~0.976 lambda/r0 (e.g., Racine 1996, PASP 699, 108). # In SBKolmogorov.cpp we refine this factor to 0.9758634299 @@ -166,6 +176,13 @@ def __init__(self, lam_over_r0=None, fwhm=None, half_light_radius=None, lam=None self._flux = float(flux) self._gsparams = GSParams.check(gsparams) + if isinstance(lam, u.Quantity): + lam = lam.to_value(u.nm) + if isinstance(r0, u.Quantity): + r0 = r0.to_value(u.m) + if isinstance(r0_500, u.Quantity): + r0_500 = r0_500.to_value(u.m) + if fwhm is not None : if any(item is not None for item in (lam_over_r0, lam, r0, r0_500, half_light_radius)): raise GalSimIncompatibleValuesError( diff --git a/galsim/phase_psf.py b/galsim/phase_psf.py index 1284659061..c459f440ff 100644 --- a/galsim/phase_psf.py +++ b/galsim/phase_psf.py @@ -20,6 +20,7 @@ from heapq import heappush, heappop import numpy as np +import astropy.units as u import copy from . import fits @@ -1807,7 +1808,7 @@ class OpticalPSF(GSObject): gsparams: An optional `GSParams` argument. [default: None] """ _opt_params = { - "diam": float, + "diam": (float, u.Quantity), "defocus": float, "astig1": float, "astig2": float, @@ -1833,7 +1834,7 @@ class OpticalPSF(GSObject): "pupil_plane_size": float, "scale_unit": str, "fft_sign": str} - _single_params = [{"lam_over_diam": float, "lam": float}] + _single_params = [{"lam_over_diam": float, "lam": (float, u.Quantity)}] _has_hard_edges = False _is_axisymmetric = False @@ -1852,6 +1853,10 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, tip=0., tilt=0., def pupil_angle=0.*radians, scale_unit=arcsec, fft_sign='+', gsparams=None, _force_stepk=0., _force_maxk=0., suppress_warning=False, geometric_shooting=False): + if isinstance(lam, u.Quantity): + lam = lam.to_value(u.nm) + if isinstance(diam, u.Quantity): + diam = diam.to_value(u.m) if fft_sign not in ['+', '-']: raise GalSimValueError("Invalid fft_sign", fft_sign, allowed_values=['+','-']) if isinstance(scale_unit, str): diff --git a/galsim/second_kick.py b/galsim/second_kick.py index 1bb5372c70..055ac1e428 100644 --- a/galsim/second_kick.py +++ b/galsim/second_kick.py @@ -18,6 +18,8 @@ __all__ = [ 'SecondKick' ] +import astropy.units as u + from . import _galsim from .gsobject import GSObject from .gsparams import GSParams @@ -84,7 +86,11 @@ class SecondKick(GSObject): construct one (e.g., 'arcsec', 'radians', etc.). [default: galsim.arcsec] gsparams: An optional `GSParams` argument. [default: None] """ - _req_params = { "lam" : float, "r0" : float, "diam" : float } + _req_params = { + "lam" : (float, u.Quantity), + "r0" : (float, u.Quantity), + "diam" : (float, u.Quantity), + } _opt_params = { "obscuration" : float, "kcrit" : float, "flux" : float, "scale_unit" : str } _has_hard_edges = False @@ -94,6 +100,13 @@ class SecondKick(GSObject): def __init__(self, lam, r0, diam, obscuration=0, kcrit=0.2, flux=1, scale_unit=arcsec, gsparams=None): + if isinstance(lam, u.Quantity): + lam = lam.to_value(u.nm) + if isinstance(r0, u.Quantity): + r0 = r0.to_value(u.m) + if isinstance(diam, u.Quantity): + diam = diam.to_value(u.m) + if isinstance(scale_unit, str): self._scale_unit = AngleUnit.from_name(scale_unit) else: diff --git a/galsim/vonkarman.py b/galsim/vonkarman.py index 1eb2ec6b6f..33e41eb8d2 100644 --- a/galsim/vonkarman.py +++ b/galsim/vonkarman.py @@ -19,6 +19,7 @@ __all__ = [ 'VonKarman' ] import numpy as np +import astropy.units as u from . import _galsim from .gsobject import GSObject @@ -102,9 +103,17 @@ class VonKarman(GSObject): keyword. [default: False] gsparams: An optional `GSParams` argument. [default: None] """ - _req_params = { "lam" : float } - _opt_params = { "L0" : float, "flux" : float, "scale_unit" : str, "do_delta" : bool } - _single_params = [ { "r0" : float, "r0_500" : float } ] + _req_params = { "lam" : (float, u.Quantity) } + _opt_params = { + "L0" : (float, u.Quantity), + "flux" : float, + "scale_unit" : str, + "do_delta" : bool + } + _single_params = [ { + "r0" : (float, u.Quantity), + "r0_500" : (float, u.Quantity) + } ] _has_hard_edges = False _is_axisymmetric = True @@ -113,6 +122,15 @@ class VonKarman(GSObject): def __init__(self, lam, r0=None, r0_500=None, L0=25.0, flux=1, scale_unit=arcsec, force_stepk=0.0, do_delta=False, suppress_warning=False, gsparams=None): + if isinstance(lam, u.Quantity): + lam = lam.to_value(u.nm) + if isinstance(r0, u.Quantity): + r0 = r0.to_value(u.m) + if isinstance(r0_500, u.Quantity): + r0_500 = r0_500.to_value(u.m) + if isinstance(L0, u.Quantity): + L0 = L0.to_value(u.m) + # We lose stability if L0 gets too large. This should be close enough to infinity for # all practical purposes though. if L0 > 1e10: diff --git a/tests/test_config_gsobject.py b/tests/test_config_gsobject.py index e76184a00d..68627e80c6 100644 --- a/tests/test_config_gsobject.py +++ b/tests/test_config_gsobject.py @@ -18,6 +18,7 @@ import logging import numpy as np +import astropy.units as u import os import sys @@ -222,6 +223,7 @@ def test_airy(): 'gsparams' : { 'xvalue_accuracy' : 1.e-2 } }, 'gal6' : { 'type' : 'Airy' , 'lam' : 400., 'diam' : 4.0, 'scale_unit' : 'arcmin' }, + 'gal7' : { 'type' : 'Airy' , 'lam' : 400*u.nm, 'diam' : '4000 mm', 'scale_unit' : 'arcmin' }, 'bad1' : { 'type' : 'Airy' , 'lam_over_diam' : 0.4, 'lam' : 400, 'diam' : 10 }, 'bad2' : { 'type' : 'Airy' , 'flux' : 1.3 }, 'bad3' : { 'type' : 'Airy' , 'lam_over_diam' : 0.4, 'obsc' : 0.3, 'flux' : 100 }, @@ -259,6 +261,8 @@ def test_airy(): gal6a = galsim.config.BuildGSObject(config, 'gal6')[0] gal6b = galsim.Airy(lam=400., diam=4., scale_unit=galsim.arcmin) gsobject_compare(gal6a, gal6b) + gal7a = galsim.config.BuildGSObject(config, 'gal7')[0] + gsobject_compare(gal6a, gal7a) # Make sure they don't match when using the default GSParams gal5c = galsim.Airy(lam_over_diam=45) @@ -298,6 +302,8 @@ def test_kolmogorov(): 'gal5' : { 'type' : 'Kolmogorov' , 'lam_over_r0' : 1, 'flux' : 50, 'gsparams' : { 'integration_relerr' : 1.e-2, 'integration_abserr' : 1.e-4 } }, + 'gal6' : { 'type' : 'Kolmogorov' , 'lam' : '400 nm', 'r0_500' : '15 cm' }, + 'gal7' : { 'type' : 'Kolmogorov' , 'lam' : '$4000*u.Angstrom', 'r0' : 0.18 }, 'bad1' : { 'type' : 'Kolmogorov' , 'fwhm' : 2, 'lam_over_r0' : 3, 'flux' : 100 }, 'bad2' : { 'type' : 'Kolmogorov', 'flux' : 100 }, 'bad3' : { 'type' : 'Kolmogorov' , 'lam_over_r0' : 2, 'lam' : 400, 'r0' : 0.15 }, @@ -334,6 +340,14 @@ def test_kolmogorov(): with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c) + gal6a = galsim.config.BuildGSObject(config, 'gal6')[0] + gal6b = galsim.Kolmogorov(lam=400*u.nm, r0_500=15*u.cm) + gsobject_compare(gal6a, gal6b) + + gal7a = galsim.config.BuildGSObject(config, 'gal7')[0] + gal7b = galsim.Kolmogorov(lam=4000*u.Angstrom, r0=0.18) + gsobject_compare(gal7a, gal7b) + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildGSObject(config, 'bad1') with assert_raises(galsim.GalSimConfigError): @@ -341,6 +355,88 @@ def test_kolmogorov(): with assert_raises(galsim.GalSimConfigError): galsim.config.BuildGSObject(config, 'bad3') + +@timer +def test_VonKarman(): + """Test various ways to build a VonKarman + """ + config = { + 'gal1' : { 'type' : 'VonKarman' , 'lam' : 500, 'r0' : 0.2 }, + 'gal2' : { 'type' : 'VonKarman' , 'lam' : 760, 'r0_500' : 0.2 }, + 'gal3' : { 'type' : 'VonKarman' , 'lam' : 450*u.nm, 'r0_500' : '20 cm', 'flux' : 1.e6, + 'ellip' : { 'type' : 'QBeta' , 'q' : 0.6, 'beta' : 0.39 * galsim.radians } + }, + 'gal4' : { 'type' : 'VonKarman' , 'lam' : 500, 'r0' : 0.2, + 'dilate' : 3, 'ellip' : galsim.Shear(e1=0.3), + 'rotate' : 12 * galsim.degrees, + 'lens' : { + 'shear' : galsim.Shear(g1=0.03, g2=-0.05), + 'mu' : 1.03, + }, + 'shift' : { 'type' : 'XY', 'x' : 0.7, 'y' : -1.2 }, + }, + 'gal5' : { 'type' : 'VonKarman' , 'lam' : 500, 'r0' : 0.2, 'do_delta' : True, + 'gsparams' : { 'integration_relerr' : 1.e-2, 'integration_abserr' : 1.e-4 } + }, + 'bad1' : { 'type' : 'VonKarman' , 'fwhm' : 2, 'lam_over_r0' : 3, 'flux' : 100 }, + 'bad2' : { 'type' : 'VonKarman', 'flux' : 100 }, + 'bad3' : { 'type' : 'VonKarman' , 'lam' : 400, 'r0' : 0.15, 'r0_500' : 0.12 }, + } + + gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] + gal1b = galsim.VonKarman(lam = 500, r0 = 0.2) + gsobject_compare(gal1a, gal1b) + + gal2a = galsim.config.BuildGSObject(config, 'gal2')[0] + gal2b = galsim.VonKarman(lam = 760, r0_500 = 0.2) + gsobject_compare(gal2a, gal2b) + + gal3a = galsim.config.BuildGSObject(config, 'gal3')[0] + gal3b = galsim.VonKarman(lam = 450*u.nm, r0_500 = 20*u.cm, flux = 1.e6) + gal3b = gal3b.shear(q = 0.6, beta = 0.39 * galsim.radians) + gsobject_compare(gal3a, gal3b) + + + gal4a = galsim.config.BuildGSObject(config, 'gal4')[0] + gal4b = galsim.VonKarman(lam = 500, r0 = 0.2) + gal4b = gal4b.dilate(3).shear(e1 = 0.3).rotate(12 * galsim.degrees) + gal4b = gal4b.lens(0.03, -0.05, 1.03).shift(dx = 0.7, dy = -1.2) + gsobject_compare(gal4a, gal4b) + + gal5a = galsim.config.BuildGSObject(config, 'gal5')[0] + gsparams = galsim.GSParams(integration_relerr=1.e-2, integration_abserr=1.e-4) + gal5b = galsim.VonKarman(lam=500, r0=0.2, do_delta=True, gsparams=gsparams) + gsobject_compare(gal5a, gal5b) + + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + + +@timer +def test_secondKick(): + config = { + 'gal1' : { 'type' : 'SecondKick' , 'lam' : 500, 'r0' : 0.2, 'diam': 4.0 }, + 'gal2' : { 'type' : 'SecondKick' , 'lam' : '0.5 micron', 'r0' : '10 cm', 'diam': 8.2*u.m, 'flux' : 100 }, + 'gal3' : { 'type' : 'SecondKick' , 'lam' : '$2*450*u.nm', 'r0' : 0.2, 'diam': 4.0, 'obscuration' : 0.2, 'kcrit' : 0.1 }, + } + + gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] + gal1b = galsim.SecondKick(lam = 500, r0 = 0.2, diam=4.0) + gsobject_compare(gal1a, gal1b) + + gal2a = galsim.config.BuildGSObject(config, 'gal2')[0] + gal2b = galsim.SecondKick(lam = 0.5*u.micron, r0 = 10*u.cm, diam=8.2*u.m, flux = 100) + gsobject_compare(gal2a, gal2b) + + gal3a = galsim.config.BuildGSObject(config, 'gal3')[0] + gal3b = galsim.SecondKick(lam = 2*450, r0 = 0.2, diam=4.0, obscuration=0.2, kcrit=0.1) + gsobject_compare(gal3a, gal3b) + + @timer def test_opticalpsf(): """Test various ways to build a OpticalPSF @@ -371,10 +467,10 @@ def test_opticalpsf(): 'pupil_plane_im' : os.path.join(".","Optics_comparison_images","sample_pupil_rolled.fits"), 'pupil_angle' : 27.*galsim.degrees }, - 'gal6' : {'type' : 'OpticalPSF' , 'lam' : 874.0, 'diam' : 7.4, 'flux' : 70., + 'gal6' : {'type' : 'OpticalPSF' , 'lam' : '874 nm', 'diam' : '7.4 m', 'flux' : 70., 'aberrations' : [0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13], 'obscuration' : 0.1 }, - 'gal7' : {'type' : 'OpticalPSF' , 'lam' : 874.0, 'diam' : 7.4, 'aberrations' : []}, + 'gal7' : {'type' : 'OpticalPSF' , 'lam' : 0.874*u.micron, 'diam' : '$740*u.cm', 'aberrations' : []}, 'bad1' : {'type' : 'OpticalPSF' , 'lam' : 874.0, 'diam' : 7.4, 'lam_over_diam' : 0.2}, 'bad2' : {'type' : 'OpticalPSF' , 'lam_over_diam' : 0.2, 'aberrations' : "0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13"}, From 9603c7925630f3b2b0bb22dc07c45b4d93646b1a Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 12:50:05 -0700 Subject: [PATCH 10/15] improve coverage --- galsim/config/value.py | 2 +- tests/test_config_gsobject.py | 16 ++++++++++------ tests/test_photon_array.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/galsim/config/value.py b/galsim/config/value.py index ce26f92e2c..5848e49924 100644 --- a/galsim/config/value.py +++ b/galsim/config/value.py @@ -70,7 +70,7 @@ def ParseValue(config, key, base, value_type): pass else: raise GalSimConfigError( - "No valid value_type found for %s"%key, value_type) + "Could not parse %s as any of types %s."%(key, value_type)) # Special: if the "value_type" is GSObject, then switch over to that builder instead. if value_type is GSObject: diff --git a/tests/test_config_gsobject.py b/tests/test_config_gsobject.py index 68627e80c6..99ef076b32 100644 --- a/tests/test_config_gsobject.py +++ b/tests/test_config_gsobject.py @@ -303,7 +303,7 @@ def test_kolmogorov(): 'gsparams' : { 'integration_relerr' : 1.e-2, 'integration_abserr' : 1.e-4 } }, 'gal6' : { 'type' : 'Kolmogorov' , 'lam' : '400 nm', 'r0_500' : '15 cm' }, - 'gal7' : { 'type' : 'Kolmogorov' , 'lam' : '$4000*u.Angstrom', 'r0' : 0.18 }, + 'gal7' : { 'type' : 'Kolmogorov' , 'lam' : '$4000*u.Angstrom', 'r0' : 0.18*u.m }, 'bad1' : { 'type' : 'Kolmogorov' , 'fwhm' : 2, 'lam_over_r0' : 3, 'flux' : 100 }, 'bad2' : { 'type' : 'Kolmogorov', 'flux' : 100 }, 'bad3' : { 'type' : 'Kolmogorov' , 'lam_over_r0' : 2, 'lam' : 400, 'r0' : 0.15 }, @@ -362,11 +362,12 @@ def test_VonKarman(): """ config = { 'gal1' : { 'type' : 'VonKarman' , 'lam' : 500, 'r0' : 0.2 }, - 'gal2' : { 'type' : 'VonKarman' , 'lam' : 760, 'r0_500' : 0.2 }, - 'gal3' : { 'type' : 'VonKarman' , 'lam' : 450*u.nm, 'r0_500' : '20 cm', 'flux' : 1.e6, + 'gal2' : { 'type' : 'VonKarman' , 'lam' : 760, 'r0_500' : 0.2, 'L0' : 24.0 }, + 'gal3' : { 'type' : 'VonKarman' , 'lam' : 450*u.nm, 'r0_500' : '20 cm', 'L0' : '$80*u.imperial.ft', + 'flux' : 1.e6, 'ellip' : { 'type' : 'QBeta' , 'q' : 0.6, 'beta' : 0.39 * galsim.radians } }, - 'gal4' : { 'type' : 'VonKarman' , 'lam' : 500, 'r0' : 0.2, + 'gal4' : { 'type' : 'VonKarman' , 'lam' : 500, 'r0' : 0.2*u.m, 'dilate' : 3, 'ellip' : galsim.Shear(e1=0.3), 'rotate' : 12 * galsim.degrees, 'lens' : { @@ -381,6 +382,7 @@ def test_VonKarman(): 'bad1' : { 'type' : 'VonKarman' , 'fwhm' : 2, 'lam_over_r0' : 3, 'flux' : 100 }, 'bad2' : { 'type' : 'VonKarman', 'flux' : 100 }, 'bad3' : { 'type' : 'VonKarman' , 'lam' : 400, 'r0' : 0.15, 'r0_500' : 0.12 }, + 'bad4' : { 'type' : 'VonKarman' , 'lam' : 'not_a_quantity_or_float', 'r0' : 0.2}, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -388,11 +390,11 @@ def test_VonKarman(): gsobject_compare(gal1a, gal1b) gal2a = galsim.config.BuildGSObject(config, 'gal2')[0] - gal2b = galsim.VonKarman(lam = 760, r0_500 = 0.2) + gal2b = galsim.VonKarman(lam = 760, r0_500 = 0.2, L0 = 24.0) gsobject_compare(gal2a, gal2b) gal3a = galsim.config.BuildGSObject(config, 'gal3')[0] - gal3b = galsim.VonKarman(lam = 450*u.nm, r0_500 = 20*u.cm, flux = 1.e6) + gal3b = galsim.VonKarman(lam = 450*u.nm, r0_500 = 20*u.cm, L0 = 80*u.imperial.ft, flux = 1.e6) gal3b = gal3b.shear(q = 0.6, beta = 0.39 * galsim.radians) gsobject_compare(gal3a, gal3b) @@ -414,6 +416,8 @@ def test_VonKarman(): galsim.config.BuildGSObject(config, 'bad2') with assert_raises(galsim.GalSimConfigError): galsim.config.BuildGSObject(config, 'bad3') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad4') @timer diff --git a/tests/test_photon_array.py b/tests/test_photon_array.py index fd7bd5fa5b..316f1d51d7 100644 --- a/tests/test_photon_array.py +++ b/tests/test_photon_array.py @@ -843,7 +843,7 @@ def test_dcr(): 'HA': local_sidereal_time-obj_coord.ra, 'latitude': '-30:14:23.76 deg', 'pressure': 72*u.kPa, - 'temperature': 290, #'290 K', + 'temperature': '290 K', 'H2O_pressure': '$900*u.Pa', } im5c = galsim.config.BuildImage(config) From 13347b6eafeb047fb287fefdf2db32fbb5764387 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 14:36:02 -0700 Subject: [PATCH 11/15] Update gsobject docs about Quantity --- galsim/airy.py | 12 ++++++------ galsim/config/bandpass.py | 16 ++++++++-------- galsim/kolmogorov.py | 20 +++++++++++--------- galsim/phase_psf.py | 12 ++++++------ galsim/photon_array.py | 9 ++++++--- galsim/second_kick.py | 6 +++--- galsim/vonkarman.py | 14 ++++++++------ 7 files changed, 48 insertions(+), 41 deletions(-) diff --git a/galsim/airy.py b/galsim/airy.py index 301b406edf..aa175ea0c4 100644 --- a/galsim/airy.py +++ b/galsim/airy.py @@ -64,12 +64,12 @@ class Airy(GSObject): Parameters: lam_over_diam: The parameter that governs the scale size of the profile. See above for details about calculating it. - lam: Lambda (wavelength) in units of nanometers. Must be supplied with - ``diam``, and in this case, image scales (``scale``) should be specified - in units of ``scale_unit``. - diam: Telescope diameter in units of meters. Must be supplied with - ``lam``, and in this case, image scales (``scale``) should be specified - in units of ``scale_unit``. + lam: Lambda (wavelength) either as an astropy Quantity, or as a float in units + of nanometers. Must be supplied with ``diam``, and in this case, image + scales (``scale``) should be specified in units of ``scale_unit``. + diam: Telescope diameter either as an astropy Quantity, or as a float in units of + meters. Must be supplied with ``lam``, and in this case, image scales + (``scale``) should be specified in units of ``scale_unit``. obscuration: The linear dimension of a central obscuration as a fraction of the pupil dimension. [default: 0] flux: The flux (in photons/cm^2/s) of the profile. [default: 1] diff --git a/galsim/config/bandpass.py b/galsim/config/bandpass.py index 2c6ef278b8..0057349877 100644 --- a/galsim/config/bandpass.py +++ b/galsim/config/bandpass.py @@ -111,14 +111,14 @@ def buildBandpass(self, config, base, logger): class FileBandpassBuilder(BandpassBuilder): """A class for loading a Bandpass from a file - FileBandpass expected the following parameters: - - file_name (str) The file to load (required) - wave_type(str) The units (nm or Ang) of the wavelengths in the file (required) - thin (float) A relative error to use for thinning the file (default: None) - blue_limit (float) A cutoff wavelength on the blue side (default: None) - red_limit (float) A cutoff wavelength on the red side (default: None) - zeropoint (float) A zeropoint to use (default: None) + FileBandpass expects the following parameters: + + file_name (str) The file to load (required) + wave_type (str or Quantity) The units (nm or Ang) of the wavelengths in the file (required) + thin (float) A relative error to use for thinning the file (default: None) + blue_limit (float or Quantity) A cutoff wavelength on the blue side (default: None) + red_limit (float or Quantity) A cutoff wavelength on the red side (default: None) + zeropoint (float or str) A zeropoint to use (default: None) """ def buildBandpass(self, config, base, logger): """Build the Bandpass based on the specifications in the config dict. diff --git a/galsim/kolmogorov.py b/galsim/kolmogorov.py index 615a542a64..1fa7066536 100644 --- a/galsim/kolmogorov.py +++ b/galsim/kolmogorov.py @@ -97,15 +97,17 @@ class Kolmogorov(GSObject): half_light_radius: The half-light radius of the profile. Typically given in arcsec. [One of ``lam_over_r0``, ``fwhm``, ``half_light_radius``, or ``lam`` (along with either ``r0`` or ``r0_500``) is required.] - lam: Lambda (wavelength) in units of nanometers. Must be supplied with - either ``r0`` or ``r0_500``, and in this case, image scales (``scale``) - should be specified in units of ``scale_unit``. - r0: The Fried parameter in units of meters. Must be supplied with ``lam``, - and in this case, image scales (``scale``) should be specified in units - of ``scale_unit``. - r0_500: The Fried parameter in units of meters at 500 nm. The Fried parameter - at the given wavelength, ``lam``, will be computed using the standard - relation r0 = r0_500 * (lam/500)**1.2. + lam: Lambda (wavelength) either as an astropy Quantity or a float in units of + nanometers. Must be supplied with either ``r0`` or ``r0_500``, and in + this case, image scales (``scale``) should be specified in units of + ``scale_unit``. + r0: The Fried parameter, either as an astropy Quantity or a float in units + of meters. Must be supplied with ``lam``, and in this case, image + scales (``scale``) should be specified in units of ``scale_unit``. + r0_500: The Fried parameter at wavelength of 500 nm, either as an astropy + Quantity or a float in units of meters. The Fried parameter at the + given wavelength, ``lam``, will be computed using the standard relation + r0 = r0_500 * (lam/500)**1.2. flux: The flux (in photons/cm^2/s) of the profile. [default: 1] scale_unit: Units to use for the sky coordinates when calculating lam/r0 if these are supplied separately. Note that the results of using properties diff --git a/galsim/phase_psf.py b/galsim/phase_psf.py index c459f440ff..003881ba45 100644 --- a/galsim/phase_psf.py +++ b/galsim/phase_psf.py @@ -1710,12 +1710,12 @@ class OpticalPSF(GSObject): lam_over_diam: Lambda / telescope diameter in the physical units adopted for ``scale`` (user responsible for consistency). Either ``lam_over_diam``, or ``lam`` and ``diam``, must be supplied. - lam: Lambda (wavelength) in units of nanometers. Must be supplied with - ``diam``, and in this case, image scales (``scale``) should be - specified in units of ``scale_unit``. - diam : Telescope diameter in units of meters. Must be supplied with - ``lam``, and in this case, image scales (``scale``) should be - specified in units of ``scale_unit``. + lam: Lambda (wavelength), either as an astropy Quantity or a float in units + of nanometers. Must be supplied with ``diam``, and in this case, image + scales (``scale``) should be specified in units of ``scale_unit``. + diam : Telescope diameter, either as an astropy Quantity or a float in units of + meters. Must be supplied with ``lam``, and in this case, image scales + (``scale``) should be specified in units of ``scale_unit``. tip: Tip in units of incident light wavelength. [default: 0] tilt: Tilt in units of incident light wavelength. [default: 0] defocus: Defocus in units of incident light wavelength. [default: 0] diff --git a/galsim/photon_array.py b/galsim/photon_array.py index e1edd9c11c..5de24fcaf8 100644 --- a/galsim/photon_array.py +++ b/galsim/photon_array.py @@ -1013,9 +1013,12 @@ class PhotonDCR(PhotonOp): [default: None] HA: Hour angle of the object as an `Angle`. [default: None] latitude: Latitude of the observer as an `Angle`. [default: None] - pressure: Air pressure in kiloPascals. [default: 69.328 kPa] - temperature: Temperature in Kelvins. [default: 293.15 K] - H2O_pressure: Water vapor pressure in kiloPascals. [default: 1.067 kPa] + pressure: Air pressure, either as an astropy Quantity or a float in units of + kiloPascals. [default: 69.328 kPa] + temperature: Temperature, either as an astropy Quantity or a float in units of + Kelvin. [default: 293.15 K] + H2O_pressure: Water vapor pressure, either as an astropy Quantity or a float in units + of kiloPascals. [default: 1.067 kPa] """ _req_params = { 'base_wavelength' : float } _opt_params = { 'scale_unit' : str, diff --git a/galsim/second_kick.py b/galsim/second_kick.py index 055ac1e428..1dc96bd53b 100644 --- a/galsim/second_kick.py +++ b/galsim/second_kick.py @@ -73,9 +73,9 @@ class SecondKick(GSObject): Peterson et al. 2015 ApJSS vol. 218 Parameters: - lam: Wavelength in nanometers - r0: Fried parameter in meters. - diam: Aperture diameter in meters. + lam: Wavelength, either as an astropy Quantity or a float in nanometers. + r0: Fried parameter, either as an astropy Quantity or a float in meters. + diam: Aperture diameter, either as an astropy Quantity or a float in nanmeters. obscuration: Linear dimension of central obscuration as fraction of aperture linear dimension. [0., 1.). [default: 0.0] kcrit: Critical Fourier mode (in units of 1/r0) below which the turbulence diff --git a/galsim/vonkarman.py b/galsim/vonkarman.py index 33e41eb8d2..bd2abaf2f7 100644 --- a/galsim/vonkarman.py +++ b/galsim/vonkarman.py @@ -70,12 +70,14 @@ class VonKarman(GSObject): though, then you can pass the do_delta=True argument to the VonKarman initializer. Parameters: - lam: Wavelength in nanometers. - r0: Fried parameter at specified wavelength ``lam`` in meters. Exactly one - of r0 and r0_500 should be specified. - r0_500: Fried parameter at 500 nm in meters. Exactly one of r0 and r0_500 - should be specified. - L0: Outer scale in meters. [default: 25.0] + lam: Wavelength, either as an astropy Quantity or a float in nanometers. + r0: Fried parameter at specified wavelength ``lam``, either as an astropy + Quantity or a float in meters. Exactly one of r0 and r0_500 should be + specified. + r0_500: Fried parameter at 500 nm, either as an astropy Quantity or a float in + meters. Exactly one of r0 and r0_500 should be specified. + L0: Outer scale, either as an astropy Quantity or a float in meters. + [default: 25.0 m] flux: The flux (in photons/cm^2/s) of the profile. [default: 1] scale_unit: Units assumed when drawing this profile or evaluating xValue, kValue, etc. Should be a `galsim.AngleUnit` or a string that can be used to From 7ede8c05319b505928951afb9bcba04c15899a39 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 14:36:43 -0700 Subject: [PATCH 12/15] Update sphinx docs for Quantity and Unit --- docs/conf.py | 6 ++++++ docs/config_image.rst | 6 +++--- docs/config_objects.rst | 26 ++++++++++++------------ docs/config_stamp.rst | 8 ++++---- docs/config_values.rst | 45 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b3acc0c3ef..b468d03e6f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,6 +68,7 @@ 'sphinx.ext.autosectionlabel', 'sphinx.ext.napoleon', 'sphinx.ext.coverage', + 'sphinx.ext.intersphinx', 'breathe', 'gh-link', ] @@ -77,6 +78,11 @@ breathe_default_project = "GalSim" breathe_default_members = ('members', 'undoc-members') +# Link to astropy +intersphinx_mapping = { + 'astropy': ('http://docs.astropy.org/en/stable/', None), +} + # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] diff --git a/docs/config_image.rst b/docs/config_image.rst index 8b18b1e8fa..92c86e72e2 100644 --- a/docs/config_image.rst +++ b/docs/config_image.rst @@ -344,9 +344,9 @@ other types, including custom Bandpass types. * 'FileBandpass' is the default type here, and you may omit the type name when using it. * ``file_name`` = *str_value* (required) The file to read in. - * ``wave_type`` = *str_value* (required) The unit of the wavelengths in the file ('nm' or 'Ang' or variations on these -- cf. `Bandpass`) - * ``blue_limit`` = *float_value* (optional) Hard cut off on the blue side. - * ``red_limit`` = *float value* (optional) Hard cut off on the red side. + * ``wave_type`` = *str_value* or *unit_value* (required) The unit of the wavelengths in the file ('nm' or 'Ang' or variations on these -- cf. `Bandpass`) + * ``blue_limit`` = *float_value* or *quantity_value* (optional) Hard cut off on the blue side. + * ``red_limit`` = *float value* or *quantity_value* (optional) Hard cut off on the red side. * ``zeropoint`` = *float_value* (optional) The zero-point to use. * ``thin`` = *float_value* (optional) If given, call `Bandpass.thin` on the Bandpass after reading in from the file, using this for the ``rel_err``. diff --git a/docs/config_objects.rst b/docs/config_objects.rst index ce5c15ba22..809959e923 100644 --- a/docs/config_objects.rst +++ b/docs/config_objects.rst @@ -33,17 +33,17 @@ PSF Types * 'Airy' A simple Airy disk. (Typically one would convolve this by some model of the atmospheric component of the PSF. cf. 'Convolution' below.) * ``lam_over_diam`` = *float_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required) Lambda / telescope_diameter converted to units of arcsec (or whatever units you want your profile to use). - * ``lam`` = *float_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the wavelength in nanometers. - * ``diam`` = *float_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the telescope diameter in meters. + * ``lam`` = *float_value* or *quantity_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the wavelength in nanometers. + * ``diam`` = *float_value* or *quantity_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the telescope diameter in meters. * ``obscuration`` = *float_value* (default = 0) The linear size of an obstructing secondary mirror as a fraction of the full mirror size. * ``scale_unit`` = *str_value* (default = 'arcsec') Units to be used for internal calculations when calculating lam/diam. * 'Kolmogorov' A Kolmogorov turbulent spectrum: :math:`T(k) \sim \exp(-D(k)/2)`, where :math:`D(k) = 6.8839 (\lambda k/2\pi r0)^{5/3}`. * ``lam_over_r0`` = *float_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) Lambda / r0 converted to units of arcsec (or whatever units you want your profile to use). - * ``lam`` = *float_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) The wavelength in nanometers. - * ``r0`` = *float_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) The Fried parameter in meters. - * ``r0_500`` = *float_value* (optional, in lieu of ``r0``). The Fried parameter in meters at a wavelength of 500 nm. The correct ``r0`` value will be calculated using the standard relation r0 = r0_500 * (lam/500)``1.2. + * ``lam`` = *float_value* or *quantity_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) The wavelength in nanometers. + * ``r0`` = *float_value* or *quantity_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) The Fried parameter in meters. + * ``r0_500`` = *float_value* or *quantity_value* (optional, in lieu of ``r0``). The Fried parameter in meters at a wavelength of 500 nm. The correct ``r0`` value will be calculated using the standard relation r0 = r0_500 * (lam/500)``1.2. * ``fwhm`` = *float_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) * ``half_light_radius`` = *float_value* (exactly one of ``lam_over_r0``, ``fwhm`` or ``half_light_radius`` or both ``lam`` and ``r0`` is required) * ``scale_unit`` = *str_value* (default = 'arcsec') Units to be used for internal calculations when calculating lam/r0. @@ -51,8 +51,8 @@ PSF Types * 'OpticalPSF' A PSF from aberrated telescope optics. * ``lam_over_diam`` = *float_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required) - * ``lam`` = *float_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the wavelength in nanometers. - * ``diam`` = *float_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the telescope diameter in meters. + * ``lam`` = *float_value* or *quantity_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the wavelength in nanometers. + * ``diam`` = *float_value* or *quantity_value* (either ``lam_over_diam`` or both ``lam`` and ``diam`` required). This should be the telescope diameter in meters. * ``defocus`` = *float_value* (default = 0) The defocus value, using the Noll convention for the normalization. (Noll index 4) * ``astig1`` = *float_value* (default = 0) The astigmatism in the y direction, using the Noll convention for the normalization. (Noll index 5) * ``astig2`` = *float_value* (default = 0) The astigmatism in the x direction, using the Noll convention for the normalization. (Noll index 6) @@ -85,9 +85,9 @@ PSF Types * ``zenith_coord`` = *CelestialCoord* (optional) The (ra,dec) coordinate of the zenith. * ``HA`` = *Angle_value* (optional) Hour angle of the observation. * ``latitude`` = *Angle_value* (optional) Latitude of the observatory. - * ``pressure`` = *float_value* (default = 69.328) Air pressure in kPa. - * ``temperature`` = *float_value* (default = 293.15) Temperature in K. - * ``H2O_pressure`` = *float_value* (default = 1.067) Water vapor pressure in kPa. + * ``pressure`` = *float_value* or *quantity_value* (default = 69.328) Air pressure in kPa. + * ``temperature`` = *float_value* or *quantity_value* (default = 293.15) Temperature in K. + * ``H2O_pressure`` = *float_value* or *quantity_value* (default = 1.067) Water vapor pressure in kPa. Galaxy Types ------------ @@ -410,11 +410,11 @@ other types, including custom SED types. * 'FileSED' is the default type here, and you may omit the type name when using it. * ``file_name`` = *str_value* (required) The file to read in. - * ``wave_type`` = *str_value* (required) The unit of the wavelengths in the file ('nm' or 'Ang' or variations on these -- cf. `SED`) + * ``wave_type`` = *str_value* or *unit_value* (required) The unit of the wavelengths in the file ('nm' or 'Ang' or variations on these -- cf. `SED`) * ``flux_type`` = *str_value* (required) The type of spectral density or dimensionless normalization used in the file ('flambda', 'fnu', 'fphotons' or '1' -- cf. `SED`) * ``redshift`` = *float_value* (optional) If given, shift the spectrum to the given redshift. You can also specify the redshift as an object-level parameter if preferred. - * ``norm_flux_density`` = *float_value* (optional) Set a normalization value of the flux density at a specific wavelength. If given, ``norm_wavelength`` is required. - * ``norm_wavelength`` = *float_value* (optional) The wavelength to use for the normalization flux density. + * ``norm_flux_density`` = *float_value* or *quantity_value* (optional) Set a normalization value of the flux density at a specific wavelength. If given, ``norm_wavelength`` is required. + * ``norm_wavelength`` = *float_value* or *quantity_value* (optional) The wavelength to use for the normalization flux density. * ``norm_flux`` = *float_value* (optional) Set a normalization value of the flux over a specific bandpass. If given, ``norm_bandpass`` is required. * ``norm_bandpass`` = *Bandpass* (optional) The bandpass to use for the normalization flux. diff --git a/docs/config_stamp.rst b/docs/config_stamp.rst index 464289c09d..258bef2fb5 100644 --- a/docs/config_stamp.rst +++ b/docs/config_stamp.rst @@ -182,16 +182,16 @@ The photon operator types defined by GalSim are: * ``zenith_coord`` = *sky_value* (optional; see above) the celestial coordinates of the zenith. * ``HA`` = *angle_value* (optional; see above) the local hour angle. * ``latitude`` = *angle_value* (optional; see above) the latitude of the telescope. - * ``pressure`` = *float_value* (default = 69.328) the pressure in kPa. - * ``temperature`` = *float_value* (default = 293.15) the temperature in Kelvin. - * ``H2O_pressure`` = *float_value* (default = 1.067) the water vapor pressure in kPa. + * ``pressure`` = *float_value* or *quantity_value* (default = 69.328) the pressure in kPa. + * ``temperature`` = *float_value* or *quantity_value* (default = 293.15) the temperature in Kelvin. + * ``H2O_pressure`` = *float_value* or *quantity_value* (default = 1.067) the water vapor pressure in kPa. * 'FocusDepth' adjusts the positions of the photons at the surface of the sensor to account for the nominal focus being either above or below the sensor surface. The depth value is typically negative, since the best focus is generally somewhere in the bulk of the sensor (although for short wavelengths it is often very close to the surface). - * ``depth`` = *float_value* (required) The distance above the surface where the photons are + * ``depth`` = *float_value* (required) The distance (in pixels) above the surface where the photons are nominally in focus. A negative value means the focus in below the surface of the sensor. * 'Refraction' adjusts the incidence angles to account for refraction at the surface of the diff --git a/docs/config_values.rst b/docs/config_values.rst index 08d658d466..023119bdf1 100644 --- a/docs/config_values.rst +++ b/docs/config_values.rst @@ -13,6 +13,8 @@ have been designating using the following italic names: - *pos_value* corresponds to a GalSim `PositionD` instance. See `pos_value` - *sky_value* corresponds to a GalSim `CelestialCoord` instance. See `sky_value` - *table_value* corresponds to a GalSim `LookupTable` instance. See `table_value` +- *quantity_value* corresponds to an Astropy :class:`~astropy.units.Quantity` instance. See `quantity_value` +- *unit_value* corresponds to an Astropy :class:`~astropy.units.Unit` instance. See `unit_value` Each of the Python types can be given as a constant value using the normal Python conventions for how to specify such a value. The GalSim *angle_value* and *pos_value* also have @@ -571,6 +573,49 @@ Options are: * A string that starts with '$' or '@'. See `Shorthand Notation`. +quantity_value +-------------- + +These represent `astropy.units.Quantity` values, which are a combination of a float and a unit (specifically, an `astropy.units.Unit`). + +Options are: + +* An `astropy.units.Quantity` value (e.g. '8.7*u.m', where 'u' is the `astropy.units` module) +* A string interpretable by `astropy.units.Quantity` (e.g. '8.7 m') +* A dict with: + + * ``type`` = *str* (required) There is only one valid option: + + * 'Quantity' Specify the value and unit separately. + + * ``value`` = *float_value* (required) + * ``unit`` = *unit_value* (required) + + * 'Eval' Evaluate a string. See `Eval type`. + +* A string that starts with '$' or '@'. See `Shorthand Notation`. + +unit_value +---------- + +These represent `astropy.units.Unit` values. + +Options are: + +* An `astropy.units.Unit` value (e.g. 'u.m', where 'u' is the `astropy.units` module) +* A string interpretable by `astropy.units.Unit` (e.g. 'm') +* A dict with: + + * ``type`` = *str* (required) There is only one valid option: + + * 'Unit' Specify the unit. + + * ``unit`` = *str_value* (required) + + * 'Eval' Evaluate a string. See `Eval type`. + +* A string that starts with '$' or '@'. See `Shorthand Notation`. + Eval type --------- From c9136d7d06310763c47b08cbef72cf8e6b1443e5 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 15:10:26 -0700 Subject: [PATCH 13/15] Enable Quanity/Unit in eval_variables --- galsim/config/value.py | 2 +- galsim/config/value_eval.py | 2 ++ tests/test_config_value.py | 10 +++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/galsim/config/value.py b/galsim/config/value.py index 5848e49924..2ba2411fec 100644 --- a/galsim/config/value.py +++ b/galsim/config/value.py @@ -851,7 +851,7 @@ def RegisterValueType(type_name, gen_func, valid_types, input_type=None): [ float, int, bool, str, Angle, Shear, PositionD, CelestialCoord, LookupTable ]) RegisterValueType('Current', _GenerateFromCurrent, [ float, int, bool, str, Angle, Shear, PositionD, CelestialCoord, LookupTable, - dict, list, None, Quantity ]) + dict, list, None, Quantity, Unit ]) RegisterValueType('Sum', _GenerateFromSum, [ float, int, Angle, Shear, PositionD, Quantity ]) RegisterValueType('Sequence', _GenerateFromSequence, [ float, int, bool ]) RegisterValueType('NumberedFile', _GenerateFromNumberedFile, [ str ]) diff --git a/galsim/config/value_eval.py b/galsim/config/value_eval.py index 43d762dd81..87c594bf79 100644 --- a/galsim/config/value_eval.py +++ b/galsim/config/value_eval.py @@ -44,6 +44,8 @@ 'd' : dict, 'l' : list, 'x' : None, + 'q' : Quantity, + 'u' : Unit, } diff --git a/tests/test_config_value.py b/tests/test_config_value.py index 0078150caf..e0acbfc56e 100644 --- a/tests/test_config_value.py +++ b/tests/test_config_value.py @@ -17,6 +17,7 @@ # import numpy as np +import astropy.units as u import math import galsim @@ -1686,6 +1687,8 @@ def test_eval(): 'ddct' : { 'a' : 1, 'b' : 2 }, 'llst' : [ 1.5, 1.0, 0.5 ], 'xlit_two' : 2, + 'qlength' : 1.8*u.m, + 'upint' : u.imperial.pint, }, # Shorthand notation with $ 'eval4' : '$np.exp(-0.5 * y**2)', @@ -1713,11 +1716,12 @@ def test_eval(): # Can access the input object as a current. 'eval21' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * ((@input.catalog).nobjects*0.6)**2)' }, 'eval22' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * (@input.dict["f"]*18)**2)' }, + 'eval23' : { 'type' : 'Eval', 'str' : 'np.exp(-pint/u.imperial.quart * length.to_value(u.m)**2)' }, # Some that raise exceptions 'bad1' : { 'type' : 'Eval', 'str' : 'npexp(-0.5)' }, 'bad2' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * x**2)', 'x' : 1.8 }, - 'bad3' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * x**2)', 'qx' : 1.8 }, + 'bad3' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * x**2)', 'wx' : 1.8 }, 'bad4' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * q**2)', 'fx' : 1.8 }, 'bad5' : { 'type' : 'Eval', 'eval_str' : 'np.exp(-0.5 * x**2)', 'fx' : 1.8 }, @@ -1753,14 +1757,14 @@ def test_eval(): galsim.config.ProcessInput(config) true_val = np.exp(-0.5 * 1.8**2) # All of these should equal this value. - for i in range(1,23): + for i in range(1,24): test_val = galsim.config.ParseValue(config, 'eval%d'%i, config, float)[0] print('i = ',i, 'val = ',test_val,true_val) np.testing.assert_almost_equal(test_val, true_val) # Doing it again uses saved _value and _fn galsim.config.RemoveCurrent(config) - for i in range(1,22): + for i in range(1,24): test_val = galsim.config.ParseValue(config, 'eval%d'%i, config, float)[0] print('i = ',i, 'val = ',test_val,true_val) np.testing.assert_almost_equal(test_val, true_val) From e9a7f0429e35ae123f9df1d65237b7b48c3a09be Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Mon, 16 Sep 2024 15:15:40 -0700 Subject: [PATCH 14/15] Add Quantity to demo11 --- examples/demo11.py | 8 ++++++-- examples/demo11.yaml | 11 +++++++---- examples/json/demo11.json | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/demo11.py b/examples/demo11.py index 4a62a74251..4381bd2814 100644 --- a/examples/demo11.py +++ b/examples/demo11.py @@ -81,6 +81,7 @@ def main(argv): observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ + import astropy.units as u logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") @@ -95,7 +96,10 @@ def main(argv): # (This corresponds to 8 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) - tel_diam = 4 # Let's figure out the flux for a 4 m class telescope + tel_diam = 400*u.cm # Let's figure out the flux for a 4 m class telescope. We'll + # also show how astropy units can be used here (see especially + # the eval_variables section of the yaml version of this + # demo). exp_time = 300 # exposing for 300 seconds. center_ra = 19.3*galsim.hours # The RA, Dec of the center of the image on the sky center_dec = -33.1*galsim.degrees @@ -106,7 +110,7 @@ def main(argv): # is 2.4 but there is obscuration (a linear factor of 0.33). Here, we assume that the telescope # we're simulating effectively has no obscuration factor. We're also ignoring the pi/4 factor # since it appears in the numerator and denominator, so we use area = diam^2. - hst_eff_area = 2.4**2 * (1.-0.33**2) + hst_eff_area = (2.4*u.m)**2 * (1.-0.33**2) flux_scaling = (tel_diam**2/hst_eff_area) * exp_time # random_seed is used for both the power spectrum realization and the random properties diff --git a/examples/demo11.yaml b/examples/demo11.yaml index 2441c1dd5d..8913e95042 100644 --- a/examples/demo11.yaml +++ b/examples/demo11.yaml @@ -44,6 +44,7 @@ # - image.noise : symmetrize # - wcs type : Tan(dudx, dudy, dvdx, dvdy, units, origin, ra, dec) # - top level field eval_variables +# - quantity letter type for eval variable. # # - Power spectrum shears and magnifications for non-gridded positions. # - Reading a compressed FITS image (using BZip2 compression). @@ -74,8 +75,10 @@ eval_variables : # An unusual prefix: a = Angle. atheta : &theta 0.17 degrees - # tel_diam = the telescope diameter in meters. - ftel_diam : &tel_diam 4 + # tel_diam = the telescope diameter. Prefixing this with a 'q' means it is an + # astropy.units.Quantity. We'll specify centimeters here, note the interaction with + # meters down below when we use it to calculate the scale_flux. + qtel_diam : &tel_diam 400 cm # exp_time = the exposure time in seconds. fexp_time : &exp_time 300 @@ -167,7 +170,7 @@ gal : # noise_pad_size = 40 * sqrt(2) * 0.2 arcsec/pixel = 11.3 arcsec noise_pad_size : 11.3 - # We will select a galaxy at random from the catalog. One could easily do this by setting + # We will select a galaxy at random from the catalog. One could easily do this by setting # index : { type : Random } # but we will instead allow the catalog to choose a random galaxy for us. It will remove any # selection effects involved in postage stamp creation using weights that are stored in the @@ -192,7 +195,7 @@ gal : # second exposures. The COSMOS galaxies have their flux set to be appropriate for HST # (a 2.4 m telescope with a linear obscuration factor of 0.33) with 1 second exposures. # So the flux should be scaled by (4**2/(2.4*(1-0.33**2))) * 300 - "$(tel_diam**2 / (2.4**2*(1.-0.33**2))) * exp_time" + "$(tel_diam**2 / ((2.4*u.m)**2*(1.-0.33**2))) * exp_time" # Define some other information about the images diff --git a/examples/json/demo11.json b/examples/json/demo11.json index 16b856fa3a..2a1ecf003d 100644 --- a/examples/json/demo11.json +++ b/examples/json/demo11.json @@ -19,7 +19,7 @@ "eval_variables" : { "fpixel_scale" : 0.2, "atheta" : "0.17 degrees", - "ftel_diam" : 4, + "qtel_diam" : "400 cm", "fexp_time" : 300, "fimage_size" : 2048, "inobjects" : 288, @@ -44,7 +44,7 @@ "shear" : { "type" : "PowerSpectrumShear" }, "magnification" : { "type" : "PowerSpectrumMagnification" }, "rotation" : { "type" : "Random" }, - "scale_flux" : "$(tel_diam**2 / (2.4**2*(1.-0.33**2))) * exp_time" + "scale_flux" : "$(tel_diam**2 / ((2.4*u.m)**2*(1.-0.33**2))) * exp_time" }, "image" : { From 35de43947f16bdb29659710edb3dd70c5dbe1365 Mon Sep 17 00:00:00 2001 From: Josh Meyers Date: Tue, 17 Sep 2024 12:04:23 -0700 Subject: [PATCH 15/15] Fix doc errors --- docs/config_values.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/config_values.rst b/docs/config_values.rst index 023119bdf1..c91fa96f8a 100644 --- a/docs/config_values.rst +++ b/docs/config_values.rst @@ -527,7 +527,7 @@ Options are: * A dict with: - * ``type`` = *str* (required) There is currently only one valid option: + * ``type`` = *str* (required) Valid options are: * 'RADec' Specify ra and dec separately. @@ -584,7 +584,7 @@ Options are: * A string interpretable by `astropy.units.Quantity` (e.g. '8.7 m') * A dict with: - * ``type`` = *str* (required) There is only one valid option: + * ``type`` = *str* (required) Valid options are: * 'Quantity' Specify the value and unit separately. @@ -606,7 +606,7 @@ Options are: * A string interpretable by `astropy.units.Unit` (e.g. 'm') * A dict with: - * ``type`` = *str* (required) There is only one valid option: + * ``type`` = *str* (required) Valid options are: * 'Unit' Specify the unit.