#!/usr/local/bin/python # # Usage: # run from shell, or # % python <this_file.py> r"""OpticalSystem module unit tests Michael Turmon, JPL, May 2016 """ import os import sys import re import numbers import unittest import inspect from copy import deepcopy from EXOSIMS.Prototypes.OpticalSystem import OpticalSystem from tests.TestSupport.Info import resource_path import numpy as np import astropy.units as u # Python 3 compatibility: if sys.version_info[0] > 2: basestring = str # # A few specs dictionaries that can be used to instantiate OpticalSystem objects # # the most basic one allowed specs_default = { 'scienceInstruments':[ {'name':'imager'}, {'name':'spectrograph'}, ], 'starlightSuppressionSystems':[ {'name':'umbrella'}, ], } # a less basic example specs_simple = { 'pupilDiam': 2.37, 'obscurFac': 0.2, 'dMag0': 25, 'intCutoff': 100, 'scienceInstruments': [ { 'name': 'imaging-EMCCD', 'QE': 0.88, 'CIC': 0.0013, 'sread': 16, 'ENF': 1.414, } ], 'starlightSuppressionSystems': [ { 'name': 'internal-imaging-HLC', 'IWA': 0.1, 'OWA': 0, 'lam': 565, 'BW': 0.10, 'core_thruput': 1, 'core_contrast': 1, 'PSF': 1 } ], } # multiple instruments + shades specs_multi = { 'pupilDiam': 2.37, 'obscurFac': 0.2, 'dMag0': 25, 'intCutoff': 100, 'scienceInstruments': [ { 'name': 'imaging-EMCCD', 'QE': 0.88, 'CIC': 0.0013, 'sread': 16, 'ENF': 1.414, }, { 'name': 'spectrograph', 'QE': 0.88, 'sread': 16, 'ENF': 1.414, } ], 'starlightSuppressionSystems': [ { 'name': 'internal-imaging-HLC', 'IWA': 0.1, 'OWA': 0, 'core_thruput': 1, 'core_contrast': 1, 'PSF': 1, 'lam': 565, 'BW': 0.10 }, { 'name': 'umbrella', 'occulter': True, 'IWA': 0.2, 'OWA': 60, 'core_contrast': 0.5, 'lam': 565, 'BW': 0.10, 'PSF': 1 } ], } # # Test Metatata # # a few of the attributes we require to be present in OpticalSystem attr_expect = ['IWA', 'OWA', 'WA0', 'dMag0', 'haveOcculter', 'intCutoff', 'obscurFac', 'observingModes', 'pupilArea', 'pupilDiam', 'scienceInstruments', 'shapeFac', 'starlightSuppressionSystems'] # This dictionary-of-dictionaries lays out a program of unit tests for # every input parameter used by the Prototype OpticalSystem. # # The outer dictionary maps parameter names (like shapeFac) to an inner # dictionary d. The inner dictionary contains fields: # default = function default value (unused by this code) # trial = list of trial values to use to instantiate the OpticalSystem object with # - for instance, if trial=(0.1,0.5), two tests are run, first with 0.1, then # with 0.5, to be sure the parameter is set. # unit = float or astropy unit # - to check the units of the parameter # target = 0, 1, or 2 # - 0 if the parameter goes in the OpticalSystem root level (OpticalSystem.shapeFac), # 1 if it goes in the scienceInstruments, 2 if it goes in starlightSuppressionSystems. # optionally: # raises = a list of exceptions raised, or None if no exception is raised # - this list is in 1:1 correspondence with the trial values in "trial", # e.g., if the third "trial" value will cause an assertion to fail, then # the third "raises" value will be AssertionError. # # Summary of possible tests # ------------------------- # Typically, the trial values are scalars. EXOSIMS does not validate the type or # range of scalars, so no value is prima facie illegal. Thus, we validate by poking # in a few test values, and being sure they are set by EXOSIMS. # # EXOSIMS does validate the scienceInstruments and starlightSuppressionSystems. We # have inserted tests to be sure that EXOSIMS raises assertions when these # attributes are not set correctly. We test that EXOSIMS plugs in correct values # for these attributes. # # The hard case is interpolants (like "QE", etc.) -- the trial values are strings # that correspond to filenames in the directory pointed to by resource_path, and # both errors and nontrivial processing can occur. # In this case, the string named in "trials" will go to EXOSIMS, and the interpolant # will be loaded by EXOSIMS from the named FITS file. # There are two kinds of interpolants: [1] functions (QE, throughput, contrast), and # [2] matrices (PSF). # [1]: The FITS files for the first kind are given as a specific (quadratic) form # ("quad100.fits", etc.). We send the file to EXOSIMS and the test code checks that # the correct values are being computed by the interpolant, by performing random probes. # The form used is 0.25 + 3*x*(1-x) for x between 0 and 1. This ranges from 0.25 to 1 and # back down to 0.25, and is smooth enough so the cubic interpolation done by EXOSIMS agrees # very closely with the actual value. # [2]: The FITS file for PSF is always given as an MxN matrix with ascending numbers, # 0...M*N-1. This allows us to check that EXOSIMS reads the FITS file correctly without # transpositions. # There is, additionally, a third, simpler, kind of interpolant -- fixed values. # This is the case where (say) contrast is given as a scalar, like 0.1. In this case, # EXOSIMS again returns an interpolant -- a function of wavelength and working angle. # But the function returns the same thing for any input. We also test this interpolant # with random probes, making sure it equals the given value. opsys_params = dict( # group GP: general optical system parameters obscurFac = dict(default=0.2, trial=(0.1, 0.5), unit=float, target=0), shapeFac = dict(default=np.pi/4, trial=(0.1, 0.5), unit=float, target=0), pupilDiam = dict(default=4.0, trial=(1.0, 10.0), unit=u.m, target=0), intCutoff = dict(default=50, trial=(10, 100), unit=u.d, target=0), # scienceInstruments: a special case. we ensure the error checking is OK for # illegal arguments -- it must be a list of dicts, each containing a 'type' key scienceInstruments = dict(default=None, trial=(None, {}, [], [{}], [{'name':'imager'}],[{'name':'imager'},{'name':'spectrograph'}]), raises=(AssertionError,)*4 + (None,)*2, unit=None, target=0), # group SIP: science instrument parameters lam = dict(default=500, trial=(400, 400.0, 600, 600.0), unit=u.nm, target=2), BW = dict(default=0.2, trial=(0.1, 0.9), unit=float, target=2), pixelSize = dict(default=13e-6, trial=(1e-6,1e-2), unit=u.m, target=1), focal = dict(default=240, trial=(200,200.0), unit=u.m, target=1), idark = dict(default=9e-5, trial=(1e-6,1e-2), unit=1/u.s, target=1), texp = dict(default=1e3, trial=(1e2, 100), unit=u.s, target=1), sread = dict(default=3, trial=(2.0,2,5), unit=float, target=1), CIC = dict(default=0.0013, trial=(0.1,0.01), unit=float, target=1), ENF = dict(default=1, trial=(1.5,2), unit=float, target=1), PCEff = dict(default=1, trial=(0.5,1.0), unit=float, target=1), # [the following test fails because Rs is now [10/2016] assigned conditionally] #Rs = dict(default=70, trial=(50,50.0), unit=float, target=1), # QE: a couple of constant values, the quadratic interpolants, and # various error values. # This pattern is followed for throughput and contrast. QE = dict(default=0.9, trial=(0.1, 0.2, "i_quad100.fits", "i_quad100t.fits", # errors follow "i_err_nofile.fits", "i_err_3d.fits", "i_err_2d_bad.fits", "i_err_2d_bad_t.fits", "i_err_negative.fits", ), raises=(None,)*4 + (AssertionError,)*5, unit=1/u.photon, target=1), # starlightSuppressionSystems: a special case. we ensure the error checking is OK for # illegal arguments -- it must be a list of dicts, each containing a 'type' key # the last trials are minimal acceptable systems starlightSuppressionSystems = dict(default=None, trial=(None, {}, [], [{}], [{'name':'parasol'}], [{'name':'parasol'},{'name':'umbrella'}]), raises=(AssertionError,)*4 + (None,)*2, unit=None, target=0), # group SSP: starlight suppression parameters core_thruput = dict(default=1e-2, trial=(0.02,0.04, "i_quad100.fits","i_quad100t.fits", # errors follow "err_nofile.fits", "err_3d.fits", "i_err_2d_bad.fits", "i_err_2d_bad_t.fits", "i_err_negative.fits" ), raises=(None,)*4 + (AssertionError,)*5, unit=float, target=2), core_contrast = dict(default=1e-9, trial=(1e-8,1e-6, "i_quad100.fits","i_quad100t.fits", # errors follow "err_nofile.fits", "err_3d.fits", "i_err_2d_bad.fits", "i_err_2d_bad_t.fits", "i_err_negative.fits", ), raises=(None,)*4 + (AssertionError,)*5, unit=float, target=2), # PSF is slightly different. The fixed trial values are numpy # matrices, and two FITS files as described above. There are also # four types of error-causing values. PSF = dict(default=np.ones((3,3)), trial=(np.random.rand(3,3), np.random.rand(5,5), "psf_5x5.fits", "psf_11x11.fits", # errors follow np.zeros((0,0)), np.zeros((3,3)), "err_3d.fits", "err_psf_zero.fits"), raises=(None,)*4 + (AssertionError,)*4, unit=float, target=2), core_platescale = dict(default=10, trial=(), unit=u.arcsec, target=2), ohTime = dict(default=1, trial=(), unit=u.d, target=2), timeMultiplier = dict(default=1, trial=(), unit=float, target=2), # group FP: fundamental IWA, OWA, dMag0 # -- related to SSP parameters IWA = dict(default=None, trial=(1,10), unit=u.arcsec, target=0), OWA = dict(default=None, trial=(10,100), unit=u.arcsec, target=0), dMag0 = dict(default=None, trial=(18.0, 22.0), unit=float, target=0), ) class TestOpticalSystemMethods(unittest.TestCase): r"""Test OpticalSystem class.""" # display of assert-specific as well as generic message longMessage = True def setUp(self): self.fixture = OpticalSystem def tearDown(self): pass def validate_basic(self, optsys, spec={}): r"""Basic validation of an OpticalSystem object.""" # check for presence of some class attributes for att in attr_expect: self.assertIn(att, optsys.__dict__) self.assertIsNotNone(optsys.__dict__[att]) # optionally, check against a supplied reference dictionary for (att,val) in spec.items(): self.assertIn(att, optsys.__dict__) val_e = optsys.__dict__[att] if isinstance(val_e, u.quantity.Quantity): # val has no units self.assertEqual(val_e.value, val) elif isinstance(val_e, dict): # weak check self.assertEqual(type(val_e), type(val)) elif isinstance(val_e, list): # weak check self.assertEqual(type(val_e), type(val)) self.assertEqual(len(val_e), len(val)) else: self.assertEqual(val_e, val) def test_init(self): r"""Test of initialization and __init__ -- simple. Method: Instantiate OpticalSystem objects from a few known-correct specs lists, and verify that the object is valid and expected fields are in place. """ for specs in [specs_default, specs_simple, specs_multi]: # the input dict is modified in-place -- so copy it optsys = self.fixture(**deepcopy(specs)) self.validate_basic(optsys, specs) def test_init_occulter(self): r"""Test of initialization and __init__ -- occulter. Method: If any starlight suppression system has an occulter , the attribute OpticalSystem.haveOcculter is set. We instantiate OpticalSystem objects and verify that this is done. """ our_specs = deepcopy(specs_default) optsys = self.fixture(**deepcopy(our_specs)) self.assertFalse(optsys.haveOcculter,'Expect to NOT haveOcculter') our_specs['starlightSuppressionSystems'][0]['occulter'] = True optsys = self.fixture(**deepcopy(our_specs)) self.assertTrue(optsys.haveOcculter, 'Expect to haveOcculter') optsys = self.fixture(**deepcopy(specs_multi)) self.assertTrue(optsys.haveOcculter, 'Expect to haveOcculter') def test_init_owa_inf(self): r"""Test of initialization and __init__ -- OWA. Method: An affordance to allow you to set OWA = +Infinity from a JSON specs-file is offered by OpticalSystem: if OWA is supplied as 0, it is set to +Infinity. We instantiate OpticalSystem objects and verify that this is done. """ for specs in [specs_default, specs_simple, specs_multi]: # the input dict is modified in-place -- so copy it our_specs = deepcopy(specs) our_specs['OWA'] = 0 for syst in our_specs['starlightSuppressionSystems']: syst['OWA'] = 0 optsys = self.fixture(**deepcopy(our_specs)) self.assertTrue(np.isposinf(optsys.OWA.value)) for syst in optsys.starlightSuppressionSystems: self.assertTrue(np.isposinf(syst['OWA'].value)) # repeat, but allow the special value to propagate up for specs in [specs_default, specs_simple, specs_multi]: # the input dict is modified in-place -- so copy it our_specs = deepcopy(specs) for syst in our_specs['starlightSuppressionSystems']: syst['OWA'] = 0 optsys = self.fixture(**deepcopy(our_specs)) self.assertTrue(np.isposinf(optsys.OWA.value)) def test_init_iwa_owa(self): r"""Test of initialization and __init__ -- IWA, OWA. Method: We instantiate OpticalSystem objects and verify various IWA/OWA relationships. """ for specs in [specs_default, specs_simple, specs_multi]: # the input dict is modified in-place -- so copy it our_specs = deepcopy(specs) # set root object IWA and OWA in conflict our_specs['IWA'] = 10 our_specs['OWA'] = 1 with self.assertRaises(AssertionError): optsys = self.fixture(**deepcopy(our_specs)) for specs in [specs_default, specs_simple, specs_multi]: # various settings of sub-object IWA and OWA for IWA, OWA in zip([1, 1, 10, 10, 20, 20], [5, 15, 5, 15, 5, 15]): # the input dict is modified in-place -- so copy it our_specs = deepcopy(specs) # set sub-object IWA and OWA for syst in our_specs['starlightSuppressionSystems']: syst['IWA'] = IWA syst['OWA'] = OWA if IWA < OWA: # will succeed in this case optsys = self.fixture(**deepcopy(our_specs)) # they must propagate up to main object self.assertTrue(optsys.OWA.value == OWA) self.assertTrue(optsys.IWA.value == IWA) else: # they propagate up, and cause a failure with self.assertRaises(AssertionError): optsys = self.fixture(**deepcopy(our_specs)) def test_init_iwa_owa_contrast(self): r"""Test of initialization and __init__ -- IWA, OWA vs. contrast domain constraint. Method: We instantiate OpticalSystem objects and verify that IWA and OWA vary as expected with the domain of WA of the contrast lookup table (from 0 to 1). """ filename = os.path.join(resource_path(), 'OpticalSystem', 'i_quad100.fits') Cmin = 0.25 # the minimum of the above lookup table is 0.25 expected_dmaglim = -2.5 * np.log10(Cmin) # the input dict is modified in-place -- so copy it our_specs = deepcopy(specs_default) for (IWA,OWA) in zip([0.0,0.2,0.5,1.1],[1.0,1.4,1.6,2.0]): for syst in our_specs['starlightSuppressionSystems']: syst['core_contrast'] = filename syst['IWA'] = IWA syst['OWA'] = OWA if IWA <= 1.0: optsys = self.fixture(**deepcopy(our_specs)) # Check that the range constraint of the contrast lookup table # (it covers WA in 0 to 1) does constrain the system OWA and IWA. self.assertTrue(optsys.OWA.value == min(1.0, OWA), msg='contrast lookup table OWA') self.assertTrue(optsys.IWA.value == max(0.0, IWA), msg='contrast lookup table IWA') else: # IWA > 1 but lookup table covers [0,1] -- conflict with self.assertRaises(AssertionError): optsys = self.fixture(**deepcopy(our_specs)) def test_init_iwa_owa_throughput(self): r"""Test of initialization and __init__ -- IWA, OWA vs. throughput domain constraint. Method: We instantiate OpticalSystem objects and verify that IWA and OWA vary as expected with the domain of WA of the throughput lookup table (from 0 to 1). """ filename = os.path.join(resource_path(), 'OpticalSystem', 'i_quad100.fits') our_specs = deepcopy(specs_default) for (IWA,OWA) in zip([0.0,0.2,0.5,1.1],[1.0,1.4,1.6,2.0]): for syst in our_specs['starlightSuppressionSystems']: syst['core_thruput'] = filename syst['IWA'] = IWA syst['OWA'] = OWA if IWA <= 1.0: optsys = self.fixture(**deepcopy(our_specs)) # Check that the range constraint of the throughput lookup table # (it covers WA in 0 to 1) does constrain the system OWA and IWA. self.assertTrue(optsys.OWA.value == min(1.0, OWA)) self.assertTrue(optsys.IWA.value == max(0.0, IWA)) else: # IWA > 1 but lookup table covers [0,1] -- conflict with self.assertRaises(AssertionError): optsys = self.fixture(**deepcopy(our_specs)) @unittest.skip('PSF sampling handling needs to be updated.') def test_init_psf(self): r"""Test of initialization and __init__ -- PSF Method: We instantiate OpticalSystem objects and verify that IWA and OWA vary as expected with the domain of WA of the throughput lookup table (from 0 to 1). """ filename = os.path.join(resource_path(), 'OpticalSystem', 'psf_5x5.fits') sampling = 1.234e-5 * u.arcsec # sampling rate keyword in above file for specs in [specs_default]: # the input dict is modified in-place -- so copy it our_specs = deepcopy(specs_default) for syst in our_specs['starlightSuppressionSystems']: syst['PSF'] = filename optsys = self.fixture(**deepcopy(our_specs)) # Check that the sampling rate is correct self.assertEqual(optsys.starlightSuppressionSystems[0]['samp'], sampling) # Check that the PSF is present and has right size # Values are checked elsewhere psf = optsys.starlightSuppressionSystems[0]['PSF'](1.0,1.0) self.assertIsInstance(psf, np.ndarray) self.assertEqual(psf.shape, (5,5)) def outspec_compare(self, outspec1, outspec2): r"""Compare two _outspec dictionaries. This is in service of the roundtrip comparison, test_roundtrip.""" self.assertEqual(sorted(list(outspec1)), sorted(list(outspec2))) for k in outspec1: if isinstance(outspec1[k], list): # this happens for scienceInstrument and starlightSuppression, # which are lists of dictionaries for (d1, d2) in zip(outspec1[k], outspec2[k]): for kk in d1: if kk.split("_")[0] == 'koAngles': self.assertEqual(d1[kk][0], d2[kk][0]) self.assertEqual(d1[kk][1], d2[kk][1]) else: self.assertEqual(d1[kk], d2[kk]) else: # these are strings or numbers, but not Quantity's, # because the outspec does not contain Quantity's self.assertEqual(outspec1[k], outspec2[k]) def test_roundtrip(self): r"""Test of initialization and __init__ -- round-trip parameter check. Method: Instantiate an OpticalSystem, use its resulting _outspec to instantiate another OpticalSystem. Assert that all quantities in each dictionary are the same. This checks that OpticalSystem objects are in fact reproducible from the _outspec alone. """ optsys = self.fixture(**deepcopy(specs_simple)) self.validate_basic(optsys, specs_simple) # save the _outspec outspec1 = optsys._outspec # recycle the _outspec into a new OpticalSystem optsys_next = self.fixture(**deepcopy(outspec1)) # this is the new _outspec outspec2 = optsys_next._outspec # ensure the two _outspec's are the same self.outspec_compare(outspec1, outspec2) def make_interpolant(self, param, value, unit): """Make an interpolating function.""" # the generic quadratic function we have used to test # always positive, reaches a maximum of 1.0 at x=0.5 quadratic = lambda x: 0.25 + 3.0 * (1-x) * x if isinstance(value, (numbers.Number, np.ndarray)): if param == 'QE': return lambda lam: value*unit elif param in ('core_thruput', 'core_contrast', 'PSF'): # for PSF, will be an ndarray return lambda lam, WA: value*unit elif isinstance(value, basestring): # for most ty if param == 'QE': return lambda lam: quadratic(lam)*unit elif param in ('core_thruput', 'core_contrast'): return lambda lam, WA: quadratic(WA)*unit elif param == 'PSF': # this rather messy construct uses a value like # "psf_5x5.fits" to recover a PSF matrix to use, else, # it uses a fixed value. The pattern matches # psf_NNNxMMM.fits where NNN and MMM are digit sequences. m = re.search("psf_(\d+)x(\d+)\.fits", value) if m is None: # use a fixed value, we won't need it anyway a_value = np.array([1]) else: # this is the size, like [5,5] s = [int(n) for n in m.groups()] # this is the value, which is always a progression # from 0...prod(s)-1, reshaped to be the size asked for a_value = np.arange(np.prod(s)).reshape(s) return lambda lam, WA: a_value*unit else: assert False, "unknown interpolant needed" def compare_interpolants(self, f1, f2, param, msg=''): r"""Compare two interpolants f1 and f2 by probing them randomly.""" # Find out the number of input arguments expected f1_info = inspect.getargspec(f1) f2_info = inspect.getargspec(f2) # this is the number of formal arguments MINUS the number of defaults # (EXOSIMS uses defaults to provide functional closure, though it's unneeded) nargin1 = len(f1_info.args) - (0 if not f1_info.defaults else len(f1_info.defaults)) nargin2 = len(f2_info.args) - (0 if not f2_info.defaults else len(f2_info.defaults)) if nargin1 != nargin2: raise self.failureException(msg + '-- functions have different arity (arg lengths)') # make a few random probes of the interpolant on the interval (0,1) for count in range(10): # obtain a vector of length nargin1 arg_in = np.random.random(nargin1) # the result can be a float (for contrast), # a numpy array (for PSF), or a Quantity (for QE) if param in ('core_thruput', 'core_contrast'): out_1 = f1(arg_in[0]*u.nm,arg_in[1]*u.arcsec) else: out_1 = f1(*arg_in) out_2 = f2(*arg_in) diff = out_1 - out_2 # if it's a quantity, unbox the difference if isinstance(diff, u.quantity.Quantity): diff = diff.value if np.any(np.abs(diff) > 1e-5): errmsg = msg + '-- function mismatch: %r != %r' % (out_1, out_2) raise self.failureException(errmsg) def compare_lists(self, list1, list2, msg=''): r"""Compare two lists-of-dicts f1 and f2 to ensure f2 attributes are in f1.""" if len(list1) != len(list2): raise self.failureException(msg + ' -- list length mismatch: %d vs %d' % (len(f1), len(f2))) for d1, d2 in zip(list1, list2): if type(d1) != type(d2): raise self.failureException(msg + ' -- type mismatch: %d vs %d' % (type(d1), type(d2))) assert isinstance(d2, dict), msg + " -- compare_lists expects lists-of-dicts" # note: we need d2 to be a subset of d1 for k in d2: self.assertEqual(d1[k], d2[k], msg + ' -- key %s mismatch' % k) @unittest.skip('All of these need to be tested separately') def test_init_sweep_inputs(self): r"""Test __init__ method, sweeping over all parameters. Method: Driven by the table at the top of this file, we sweep through all input parameters to OpticalSystem, and in each case, we instantiate an OpticalSystem with the given parameter, and check that the parameter is properly set. Additionally, for interpolants, we check that the proper function is obtained by EXOSIMS. Additionally, also for file-based interpolants, we ensure that appropriate exceptions are raised for inaccessible files and for out-of-range values.""" # iterate over all tests for (param,recipe) in opsys_params.iteritems(): print(param) # one inner loop tests that we can set 'param' # example simple param dictionary: # pupilDiam = dict(default=4.0, trial=(1.0, 10.0), unit=u.m) # set up "raises" key, if not given if 'raises' not in recipe: recipe['raises'] = (None,) * len(recipe['trial']) assert len(recipe['raises']) == len(recipe['trial']), \ 'recipe length mismatch for ' + param # target dictionary: 0 for top-level, 1 for inst, 2 for starlight target = recipe['target'] # the unit is multiplied by the numerical value when comparing unit = 1.0 if recipe['unit'] is float else recipe['unit'] # loop over all trials requested for "param" for (param_val,err_val) in zip(recipe['trial'], recipe['raises']): # need a copy because we are about to alter the specs specs = deepcopy(specs_simple) # print param, '<--', param_val, '[', err_val, ']' # make strings into full filenames if isinstance(param_val, basestring): param_val = os.path.join(resource_path(), 'OpticalSystem', param_val) # insert param_val into approriate slot within specs - a bit messy if target == 0: specs[param] = param_val elif target == 1: specs['scienceInstruments'][0][param] = param_val elif target == 2: specs['starlightSuppressionSystems'][0][param] = param_val else: # this is a failure of the "recipe" dictionary entry assert False, "Target must be 0, 1, 2 -- " + param # prepare to get an OpticalSystem if err_val is None: # no error expected in this branch # note: deepcopy specs because EXOSIMS alters them, # which would in turn alter our reference param_val # if it's mutable! optsys = self.fixture(**deepcopy(specs)) # find the right place to seek the param we tried if target == 0: d = optsys.__dict__ elif target == 1: d = optsys.__dict__['scienceInstruments'][0] elif target == 2: d = optsys.__dict__['starlightSuppressionSystems'][0] # check the value: scalar or interpolant if hasattr(d[param], '__call__'): # it's an interpolant -- make an interpolant ourselves param_int = self.make_interpolant(param, param_val, unit) # compare our interpolant with the EXOSIMS interpolant self.compare_interpolants(d[param], param_int, param) elif isinstance(d[param], list): # a small list-of-dict's in the case of the # 'scienceInstruments' and 'starlightSuppressionSystems' checks. # this checks that param_val is a subset of d[param] self.compare_lists(d[param], param_val, param) else: # a number or Quantity - simple assertion if isinstance(param_val, list): print('***', param) print(d[param]) self.assertEqual(d[param], param_val*unit, msg='failed to set "%s" parameter' % param) else: # else, ensure the right error is raised with self.assertRaises(err_val): self.fixture(**deepcopy(specs)) if __name__ == '__main__': unittest.main()