def test_comparison(self): lq1 = u.Magnitude(np.arange(1., 4.) * u.Jy) lq2 = u.Magnitude(2. * u.Jy) assert np.all((lq1 > lq2) == np.array([True, False, False])) assert np.all((lq1 == lq2) == np.array([False, True, False])) lq3 = u.Dex(2. * u.Jy) assert np.all((lq1 > lq3) == np.array([True, False, False])) assert np.all((lq1 == lq3) == np.array([False, True, False])) lq4 = u.Magnitude(2. * u.m) assert not (lq1 == lq4) assert lq1 != lq4 with pytest.raises(u.UnitsError): lq1 < lq4 q5 = 1.5 * u.Jy assert np.all((lq1 > q5) == np.array([True, False, False])) assert np.all((q5 < lq1) == np.array([True, False, False])) with pytest.raises(u.UnitsError): lq1 >= 2. * u.m with pytest.raises(u.UnitsError): lq1 <= lq1.value * u.mag # For physically dimensionless, we can compare with the function unit. lq6 = u.Magnitude(np.arange(1., 4.)) fv6 = lq6.value * u.mag assert np.all(lq6 == fv6) # but not some arbitrary unit, of course. with pytest.raises(u.UnitsError): lq6 < 2. * u.m
def changed_value(self, ref, param, unit): new_val = self.refs[ref].value if unit is not None: new_val = new_val * unit if ref == "exp_slider": new_val = pre_encode(new_val.to(u.s)) elif ref == "age_slider": new_val = pre_encode(u.Dex(10.**new_val * u.Gyr)) elif ref == "metallicity_slider": new_val = pre_encode(u.Dex(new_val)) else: new_val = pre_encode(new_val) if ref == "crowding_slider": print(param, getattr(self, param), new_val) if getattr(self, param) != new_val: setattr(self, param, new_val) return True return False
class TestLogQuantityArithmetic: @pytest.mark.parametrize('other', [ 2.4 * u.mag(), 12.34 * u.ABmag, u.Magnitude(3.45 * u.Jy), u.Dex(3.), u.Dex(np.linspace(3000, 5000, 10) * u.Angstrom), u.Magnitude(6.78, 2. * u.mag) ]) @pytest.mark.parametrize('fac', [1., 2, 0.4]) def test_multiplication_division(self, other, fac): """Check that multiplication and division works as expectes""" lq_sf = fac * other assert lq_sf.unit.physical_unit == other.unit.physical_unit**fac assert_allclose(lq_sf.physical, other.physical**fac) lq_sf = other * fac assert lq_sf.unit.physical_unit == other.unit.physical_unit**fac assert_allclose(lq_sf.physical, other.physical**fac) lq_sf = other / fac assert lq_sf.unit.physical_unit**fac == other.unit.physical_unit assert_allclose(lq_sf.physical**fac, other.physical) lq_sf = other.copy() lq_sf *= fac assert lq_sf.unit.physical_unit == other.unit.physical_unit**fac assert_allclose(lq_sf.physical, other.physical**fac) lq_sf = other.copy() lq_sf /= fac assert lq_sf.unit.physical_unit**fac == other.unit.physical_unit assert_allclose(lq_sf.physical**fac, other.physical) def test_more_multiplication_division(self): """Check that multiplication/division with other quantities is only possible when the physical unit is dimensionless, and that this turns the result into a normal quantity.""" lq = u.Magnitude(np.arange(1., 11.) * u.Jy) with pytest.raises(u.UnitsError): lq * (1. * u.m) with pytest.raises(u.UnitsError): (1. * u.m) * lq with pytest.raises(u.UnitsError): lq / lq for unit in (u.m, u.mag, u.dex): with pytest.raises(u.UnitsError): lq / unit lq2 = u.Magnitude(np.arange(1, 11.)) with pytest.raises(u.UnitsError): lq2 * lq with pytest.raises(u.UnitsError): lq2 / lq with pytest.raises(u.UnitsError): lq / lq2 lq_sf = lq.copy() with pytest.raises(u.UnitsError): lq_sf *= lq2 # ensure that nothing changed inside assert (lq_sf == lq).all() with pytest.raises(u.UnitsError): lq_sf /= lq2 # ensure that nothing changed inside assert (lq_sf == lq).all() # but dimensionless_unscaled can be cancelled r = lq2 / u.Magnitude(2.) assert r.unit == u.dimensionless_unscaled assert np.all(r.value == lq2.value / 2.) # with dimensionless, normal units OK, but return normal quantities tf = lq2 * u.m tr = u.m * lq2 for t in (tf, tr): assert not isinstance(t, type(lq2)) assert t.unit == lq2.unit.function_unit * u.m with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(lq2.unit.physical_unit) t = tf / (50. * u.cm) # now we essentially have the same quantity but with a prefactor of 2 assert t.unit.is_equivalent(lq2.unit.function_unit) assert_allclose(t.to(lq2.unit.function_unit), lq2._function_view * 2) @pytest.mark.parametrize('power', (2, 0.5, 1, 0)) def test_raise_to_power(self, power): """Check that raising LogQuantities to some power is only possible when the physical unit is dimensionless, and that conversion is turned off when the resulting logarithmic unit (say, mag**2) is incompatible.""" lq = u.Magnitude(np.arange(1., 4.) * u.Jy) if power == 0: assert np.all(lq**power == 1.) elif power == 1: assert np.all(lq**power == lq) else: with pytest.raises(u.UnitsError): lq**power # with dimensionless, it works, but falls back to normal quantity # (except for power=1) lq2 = u.Magnitude(np.arange(10.)) t = lq2**power if power == 0: assert t.unit is u.dimensionless_unscaled assert np.all(t.value == 1.) elif power == 1: assert np.all(t == lq2) else: assert not isinstance(t, type(lq2)) assert t.unit == lq2.unit.function_unit**power with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(u.dimensionless_unscaled) def test_error_on_lq_as_power(self): lq = u.Magnitude(np.arange(1., 4.) * u.Jy) with pytest.raises(TypeError): lq**lq @pytest.mark.parametrize('other', pu_sample) def test_addition_subtraction_to_normal_units_fails(self, other): lq = u.Magnitude(np.arange(1., 10.) * u.Jy) q = 1.23 * other with pytest.raises(u.UnitsError): lq + q with pytest.raises(u.UnitsError): lq - q with pytest.raises(u.UnitsError): q - lq @pytest.mark.parametrize( 'other', (1.23 * u.mag, 2.34 * u.mag(), u.Magnitude( 3.45 * u.Jy), u.Magnitude(4.56 * u.m), 5.67 * u.Unit(2 * u.mag), u.Magnitude(6.78, 2. * u.mag))) def test_addition_subtraction(self, other): """Check that addition/subtraction with quantities with magnitude or MagUnit units works, and that it changes the physical units appropriately.""" lq = u.Magnitude(np.arange(1., 10.) * u.Jy) other_physical = other.to(getattr(other.unit, 'physical_unit', u.dimensionless_unscaled), equivalencies=u.logarithmic()) lq_sf = lq + other assert_allclose(lq_sf.physical, lq.physical * other_physical) lq_sr = other + lq assert_allclose(lq_sr.physical, lq.physical * other_physical) lq_df = lq - other assert_allclose(lq_df.physical, lq.physical / other_physical) lq_dr = other - lq assert_allclose(lq_dr.physical, other_physical / lq.physical) @pytest.mark.parametrize('other', pu_sample) def test_inplace_addition_subtraction_unit_checks(self, other): lu1 = u.mag(u.Jy) lq1 = u.Magnitude(np.arange(1., 10.), lu1) with pytest.raises(u.UnitsError): lq1 += other assert np.all(lq1.value == np.arange(1., 10.)) assert lq1.unit == lu1 with pytest.raises(u.UnitsError): lq1 -= other assert np.all(lq1.value == np.arange(1., 10.)) assert lq1.unit == lu1 @pytest.mark.parametrize( 'other', (1.23 * u.mag, 2.34 * u.mag(), u.Magnitude( 3.45 * u.Jy), u.Magnitude(4.56 * u.m), 5.67 * u.Unit(2 * u.mag), u.Magnitude(6.78, 2. * u.mag))) def test_inplace_addition_subtraction(self, other): """Check that inplace addition/subtraction with quantities with magnitude or MagUnit units works, and that it changes the physical units appropriately.""" lq = u.Magnitude(np.arange(1., 10.) * u.Jy) other_physical = other.to(getattr(other.unit, 'physical_unit', u.dimensionless_unscaled), equivalencies=u.logarithmic()) lq_sf = lq.copy() lq_sf += other assert_allclose(lq_sf.physical, lq.physical * other_physical) lq_df = lq.copy() lq_df -= other assert_allclose(lq_df.physical, lq.physical / other_physical) def test_complicated_addition_subtraction(self): """For fun, a more complicated example of addition and subtraction.""" dm0 = u.Unit('DM', 1. / (4. * np.pi * (10. * u.pc)**2)) DMmag = u.mag(dm0) m_st = 10. * u.STmag dm = 5. * DMmag M_st = m_st - dm assert M_st.unit.is_equivalent(u.erg / u.s / u.AA) assert np.abs(M_st.physical / (m_st.physical * 4. * np.pi * (100. * u.pc)**2) - 1.) < 1.e-15
class CMD(SYOTool): tool_prefix = "cmd" save_models = ["telescope", "camera", "exposure"] save_params = { "exptime": None, #slider value #NOT YET UPDATED "snratio": None, #slider value "age": None, #slider value "crowding": None, #slider value "distance": None, #slider value, "metallicity": None, #slider value "noise": None, #slider value "spectrum_type": ("exposure", "sed_id"), "aperture": ("telescope", "aperture"), "user_prefix": None } save_dir = os.path.join(os.environ['LUVOIR_SIMTOOLS_DIR'], 'saves') #must include this to set defaults before the interface is constructed tool_defaults = { 'exptime': pre_encode(3600.0 * u.s), 'snratio': pre_encode(30.0 * u.electron**0.5), 'age': pre_encode(u.Dex(10.0 * u.Gyr)), 'crowding': pre_encode(20.0 * u.dimensionless_unscaled), #u.ABmag / u.arcsec**2), 'distance': pre_encode(1.0 * u.Mpc), 'metallicity': pre_encode(u.Dex(0.0)), 'noise': pre_encode(500.0 * u.dimensionless_unscaled), 'aperture': pre_encode(15.0 * u.m), 'spectrum_type': 'fab' } verbose = True def tool_preinit(self): """ Pre-initialize any required attributes for the interface. """ #set up holoviews & load data for datashader #note that right now the data is read from a pickle, not loaded separately; #I'm leaving load_dataset.py in the directory, but it's not used currently self.dataframe = pandas.read_pickle( os.path.join(basedir, 'data', 'cmd_frame_large_no_noise.pkl')) self.cmap_picker = ColormapPicker(rename={'colormap': 'cmap'}, name='') #initialize engine objects self.telescope = Telescope() self.camera = Camera() self.exposure = Exposure() self.telescope.add_camera(self.camera) self.camera.add_exposure(self.exposure) #set interface variables self.help_text = help_text #Formatting & interface stuff: self.format_string = interface_format self.interface_file = os.path.join(script_dir, "interface.yaml") #For saving calculations self.current_savefile = "" self.overwrite_save = False def tool_postinit(self): #update default exposure based on tool_defaults self.update_exposure_params() #Calculation methods @staticmethod def add_noise(new_frame, noise_scale): rmag = new_frame.rmag gmag = new_frame.gmag noise_basis = 10. / 10.**((30. + rmag) / 5.) # mind the 10! r_noise = np.random.normal(0.0, noise_scale, np.size(rmag)) * noise_basis g_noise = np.random.normal(0.0, noise_scale, np.size(rmag)) * noise_basis new_frame.rmag = rmag + r_noise new_frame.gmag = gmag + g_noise new_frame.grcolor = np.clip(rmag - gmag, a_min=-1.2, a_max=4.2) return new_frame def select_dataframe(self, metallicity, age): selection = lambda frame: (frame.metalindex == metallicity) & ( frame.ageindex == age) return self.dataframe.loc[selection] def select_stars( self, obj, age, metallicity, noise ): # received "obj" of type "Points" and "age" of type ordinary float #"obj" incoming is the points object if self.verbose: print("age / metallicity / noise inside select_stars", age, metallicity, noise) new_frame = self.select_dataframe(metallicity, age) noise_frame = self.add_noise(new_frame, float(noise)) cmd_points = hv.Points(noise_frame, kdims=['grcolor', 'rmag']) return cmd_points @property def _derived_snrs(self): new_snrs = np.zeros(5, dtype=float) mags = self.refs["cmd_mag_source"].data["mags"] for m, mag in enumerate(mags): self.exposure.renorm_sed(mag * u.ABmag) new_snrs[m] = self.exposure.recover('snr')[4].value return new_snrs @property def _derived_snrs_labels(self): return self._derived_snrs.astype('|S4') def crowding_limit(self, crowding_apparent_magnitude, distance): """ Calculate the crowding limit. """ aperture = pre_decode(self.aperture) g_solar = 4.487 stars_per_arcsec_limit = 10. * (aperture / 2.4)**2 # (JD1) distmod = 5. * np.log10((distance + 1e-5) * 1e6) - 5. #why this fudge? g = np.array( -self.dataframe.gmag ) # absolute magnitude in the g band - the -1 corrects for the sign flip in load_datasets (for plotting) g.sort( ) # now sorted from brightest to faintest in terms of absolute g magnitude luminosity = 10.**(-0.4 * (g - g_solar)) total_luminosity_in_lsun = np.sum(luminosity) number_of_stars = np.full_like( g, 1.0 ) # initial number of stars in each "bin" - a list of 10,000 stars total_absolute_magnitude = -2.5 * np.log10( total_luminosity_in_lsun) + g_solar apparent_brightness_at_this_distance = total_absolute_magnitude + distmod scale_factor = 10.**(-0.4 * (crowding_apparent_magnitude - apparent_brightness_at_this_distance)) cumulative_number_of_stars = np.cumsum( scale_factor * number_of_stars) # the cumulative run of luminosity in Lsun crowding_limit = np.interp(stars_per_arcsec_limit.value, cumulative_number_of_stars, g) return crowding_limit #Control methods def quantize_slider(self, ref, precision, minval, step): """ A utility function for quantizing the value of a slider. """ val = Decimal(self.refs[ref].value).quantize(Decimal(precision)) return int( ((val - Decimal(minval)) / Decimal(step)).to_integral_exact()) def age_slider_update(self): """ Send an event to the age stream. """ new_age = self.quantize_slider("age_slider", '0.01', '5.5', '0.05') self.refs["age_stream"].event(age=new_age) if self.verbose: print("Age selected by slider = {}".format(new_age)) def metallicity_slider_update(self): """ Send an event to the metallicity stream. Using Decimal to handle quantization. """ new_metallicity = self.quantize_slider("metallicity_slider", '0.1', '-2.0', '0.5') self.refs["metallicity_stream"].event(metallicity=new_metallicity) if self.verbose: print( "Metallicity selected by slider = {}".format(new_metallicity)) def noise_slider_update(self): """ Send an event to the noise stream. This slider is in steps of 50, so we can just quantize with int() instead of using Decimal. """ ival = int(self.refs["noise_slider"].value) if self.verbose: print("Inside noise_slider, will scale to: {}".format(ival)) self.refs["noise_stream"].event(noise=ival) def distance_slider_update(self): """ Update the magnitude label source for the new distance. """ val = self.refs["distance_slider"].value distmod = 5. * np.log10((val + 1e-5) * 1e6) - 5. new_mags = distmod + np.array([10., 5., 0., -5., -10.]) mlsource = self.refs['cmd_mag_source'] mlsource.data['mags'] = new_mags mlsource.data['text'] = new_mags.astype('|S4') def crowding_slider_update(self): """ Update the crowding from the slider input. """ crowding_mag = self.refs["crowding_slider"].value distance = self.refs["distance_slider"].value nstars_per_arcsec = int(10. * (self.refs['ap_slider'].value / 2.4)**2) crowding_limit = self.crowding_limit(crowding_mag, distance) confsource = self.refs["cmd_conf_source"] confsource.data = { 'top': [-crowding_limit], 'textx': [-0.8], 'texty': [-crowding_limit - 0.2], 'text': ['Crowding: ({} stars / sq. arsec)'.format(nstars_per_arcsec)] } def update_exposure_params(self): """ Update the exposure parameters. """ new_aper = self.refs["ap_slider"].value * u.m new_expt = (self.refs["exp_slider"].value * u.h).to(u.s) new_snr = self.refs["snr_slider"].value * u.electron**0.5 self.aperture = pre_encode(new_aper) self.exptime = pre_encode(new_expt) self.snratio = pre_encode(new_snr) self.telescope.aperture = self.aperture def exposure_update(self): """ Update the exposure when sliders are updated. """ self.update_exposure_params() self.exposure.unknown = 'snr' self.exposure.exptime = pre_decode(self.exptime) new_snrs = self._derived_snrs etcsource = self.refs["cmd_etc_source"] mlsource = self.refs["cmd_mag_source"] etcsource.data = { "mags": mlsource.data["mags"], "snr": new_snrs, "snr_label": new_snrs.astype('|S4'), 'x': np.full(5, 3.2).tolist(), 'y': np.arange(-10.4, 10., 5., dtype=float).tolist() } if self.verbose: print("mag_values in exposure_update: {}".format( etcsource.data['y'])) print("new_snrs in exposure_update: {}".format( etcsource.data['snr'])) noise_scale_factor = int( 10000. / new_snrs[1] ) # divide an number by the S/N at AB = 5 absolute - set up to make it come out right self.refs["noise_stream"].event(noise=int(noise_scale_factor)) def sn_slider_update(self): """ Update exposure when SNR slider is changed. """ new_sn = pre_decode(self.snratio) new_expt = pre_decode(self.exptime) #new_sn = self.refs["snr_slider"].value #new_expt = self.refs["exp_slider"].value if self.verbose: print("calling sn_updater with sn = {} and exptime {}".format( new_sn, new_expt)) self.exposure.unknown = "magnitude" self.exposure.exptime = new_expt #pre_decode(self.exptime) self.exposure.snr = new_sn #pre_decode(self.snratio) vmag = self.exposure.recover('magnitude')[4].value distance = pre_decode(self.distance) distmod = 5. * np.log10((distance.value + 1e-5) * 1e6) - 5. lmsource = self.refs["cmd_lim_source"] lmsource.data = { 'mags': [distmod - vmag - 0.4], 'maglabel': ["{:4.1f}".format(vmag)], 'sn': ["{}".format(new_sn.value)], 'x_mag': [3.8], 'x_sn': [3.2] } # the 0.4 is just for display purposes (text alignment) def changed_value(self, ref, param, unit): new_val = self.refs[ref].value if unit is not None: new_val = new_val * unit if ref == "exp_slider": new_val = pre_encode(new_val.to(u.s)) elif ref == "age_slider": new_val = pre_encode(u.Dex(10.**new_val * u.Gyr)) elif ref == "metallicity_slider": new_val = pre_encode(u.Dex(new_val)) else: new_val = pre_encode(new_val) if ref == "crowding_slider": print(param, getattr(self, param), new_val) if getattr(self, param) != new_val: setattr(self, param, new_val) return True return False def controller(self, attr, old, new): """ Master controller callback. Instead of having a bunch of different callbacks, instead we're going to track which things have changed, and then call the individual update methods that are required. """ #Grab values from the inputs, tracking which have changed params = { 'exp': ('exp_slider', 'exptime', u.hour), 'age': ('age_slider', 'age', None), 'snr': ('snr_slider', 'snratio', u.electron**0.5), 'crowd': ('crowding_slider', 'crowding', u.dimensionless_unscaled), # u.ABmag / u.arcsec**2), 'dist': ('distance_slider', 'distance', u.Mpc), 'metal': ('metallicity_slider', 'metallicity', None), 'noise': ('noise_slider', 'noise', u.dimensionless_unscaled), 'aper': ('ap_slider', 'aperture', u.m) } updated = set( [par for par, args in params.items() if self.changed_value(*args)]) #Call the requisite update methods if not updated.isdisjoint({'dist', 'aper', 'exp'}): self.exposure_update() if not updated.isdisjoint({'dist', 'aper', 'exp', 'crowd'}): self.crowding_slider_update() if not updated.isdisjoint({'dist', 'aper', 'exp', 'snr'}): self.sn_slider_update() if 'noise' in updated: self.noise_slider_update() if 'dist' in updated: self.distance_slider_update() if 'metal' in updated: self.metallicity_slider_update() if 'age' in updated: self.age_slider_update() #Now, all of the plot updates should be handled by stream events, I think... #NONE OF THESE MATTER UNTIL SAVE/LOAD IS ADDED def update_toggle(self, active): if active: self.refs["user_prefix"].value = self.user_prefix self.refs["user_prefix"].disabled = True self.refs["save_button"].disabled = False self.overwrite_save = True else: self.refs["user_prefix"].disabled = False self.refs["save_button"].disabled = False self.overwrite_save = False #Save and Load def save(self): """ Save the current calculations. """ #Check for an existing save file if we're overwriting if self.overwrite_save and self.current_savefile: self.current_savefile = self.save_file(self.current_savefile, overwrite=True) else: #Set the user prefix from the bokeh interface prefix = self.refs["user_prefix"].value if not prefix.isalpha() or len(prefix) < 3: self.refs["save_message"].text = "Please include a prefix of at "\ "least 3 letters (and no other characters)." return self.user_prefix = prefix #Save the file: self.current_savefile = self.save_file() #Tell the user the filename or the error message. if not self.current_savefile: self.refs["save_message"].text = "Save unsuccessful; please " \ "contact the administrators." return self.refs["save_message"].text = "This calculation was saved with " \ "the ID {}".format(self.current_savefile) self.refs["update_save"].disabled = False def load(self): # Get the filename from the bokeh interface calcid = self.refs["load_filename"].value #Load the file code = self.load_file(calcid) if not code: #everything went fine #Update the interface self.refs["update_save"].disabled = False self.current_save = calcid self.refs["exp_slider"].value = pre_decode(self.exptime).value self.refs["mag_slider"].value = pre_decode( self.renorm_magnitude).value self.refs["ap_slider"].value = pre_decode(self.aperture).value self.refs["snr_slider"].value = pre_decode(self.snratio).value temp = self.templates.index(self.spectrum_type) self.refs["template_select"].value = self.template_options[temp] self.controller(None, None, None) errmsg = [ "Calculation ID {} loaded successfully.".format(calcid), "Calculation ID {} does not exist, please try again.".format( calcid), "Load unsuccessful; please contact the administrators.", "There was an error restoring the save state; please contact" " the administrators." ][code] if code == 0 and self.load_mismatch: errmsg += "<br><br><b><i>Load Mismatch Warning:</b></i> "\ "The saved model parameters did not match the " \ "parameter values saved for this tool. Saved " \ "model values were given priority." self.refs["load_message"].text = errmsg