From 2f31b84d9ba9689aaf90a09f39d66ef6db525265 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Tue, 2 Jul 2024 16:35:12 -0400 Subject: [PATCH 01/10] Add redshift effect --- src/tdastro/base_models.py | 6 ++ src/tdastro/effects/redshift.py | 76 ++++++++++++++++++++++++++ tests/tdastro/effects/test_redshift.py | 70 ++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/tdastro/effects/redshift.py create mode 100644 tests/tdastro/effects/test_redshift.py diff --git a/src/tdastro/base_models.py b/src/tdastro/base_models.py index a0f4d363..0e24bec4 100644 --- a/src/tdastro/base_models.py +++ b/src/tdastro/base_models.py @@ -197,9 +197,15 @@ def evaluate(self, times, wavelengths, resample_parameters=False, **kwargs): if resample_parameters: self.sample_parameters(kwargs) + for effect in self.effects: + if hasattr(effect, "pre_effect"): # pre-effects == adjustments done to times and/or wavelengths + times, wavelengths = effect.pre_effect(times, wavelengths, **kwargs) + flux_density = self._evaluate(times, wavelengths, **kwargs) + for effect in self.effects: flux_density = effect.apply(flux_density, wavelengths, self, **kwargs) + return flux_density diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py new file mode 100644 index 00000000..da0fd4e8 --- /dev/null +++ b/src/tdastro/effects/redshift.py @@ -0,0 +1,76 @@ +from tdastro.base_models import EffectModel + + +class Redshift(EffectModel): + """A redshift effect model. + + This contains a "pre-effect" method, which is used to calculate the emitted wavelengths/times + needed to give us the observed wavelengths and times given the redshift. + + Attributes + ---------- + pz : `float` + The redshift. + + Notes + ----- + Conversions used are as follows: + - emitted_wavelength = observed_wavelength / (1 + redshift) + - emitted_time = observed_time / (1 + redshift) + - observed_flux = emitted_flux / (1 + redshift) + """ + + def __init__(self, pz, **kwargs): + """Create a Redshift effect model. + + Parameters + ---------- + pz : `float` + The redshift. + **kwargs : `dict`, optional + Any additional keyword arguments. + """ + super().__init__(**kwargs) + self.pz = pz + + def pre_effect(self, observed_times, observed_wavelengths, **kwargs): + """Calculate the emitted wavelengths/times needed to give us the observed wavelengths + and times given the redshift. + + Parameters + ---------- + observed_times : float + The times at which the observation is made. + observed_wavelengths : float + The wavelengths at which the observation is made. + **kwargs : `dict`, optional + Any additional keyword arguments. + + Returns + ------- + float + The adjusted flux density at the observed wavelength. + """ + return (observed_times / (1 + self.pz), observed_wavelengths / (1 + self.pz)) + + def apply(self, flux_density, bands=None, physical_model=None, **kwargs): + """Apply the effect to observations (flux_density values). + + Parameters + ---------- + flux_density : `numpy.ndarray` + An array of flux density values. + bands : `numpy.ndarray`, optional + An array of bands. + physical_model : `PhysicalModel` + A PhysicalModel from which the effect may query parameters + such as redshift, position, or distance. + **kwargs : `dict`, optional + Any additional keyword arguments. + + Returns + ------- + flux_density : `numpy.ndarray` + The results. + """ + return flux_density / (1 + self.pz) diff --git a/tests/tdastro/effects/test_redshift.py b/tests/tdastro/effects/test_redshift.py new file mode 100644 index 00000000..31da34a5 --- /dev/null +++ b/tests/tdastro/effects/test_redshift.py @@ -0,0 +1,70 @@ +import numpy as np +from tdastro.effects.redshift import Redshift +from tdastro.sources.step_source import StepSource + + +def get_no_effect_and_redshifted_values(times, wavelengths, t_start, t_end, brightness, redshift) -> tuple: + """Get the values for a source with no effects and a redshifted source.""" + model_no_effects = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) + model_redshift = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) + model_redshift.add_effect(Redshift(pz=redshift)) + + values_no_effects = model_no_effects.evaluate(times, wavelengths) + values_redshift = model_redshift.evaluate(times, wavelengths) + + # Check shape of output is as expected + assert values_no_effects.shape == (len(times), len(wavelengths)) + assert values_redshift.shape == (len(times), len(wavelengths)) + + return values_no_effects, values_redshift + + +def test_redshift() -> None: + """Test that we can create a Redshift object and it gives us values as expected.""" + times = np.array([1, 2, 3, 5, 10]) + wavelengths = np.array([100.0, 200.0, 300.0]) + t_start = 1.0 + t_end = 2.0 + brightness = 15.0 + redshift = 1.0 + + # Get the values for a redshifted step source, and a step source with no effects for comparison + (values_no_effects, values_redshift) = get_no_effect_and_redshifted_values( + times, wavelengths, t_start, t_end, brightness, redshift + ) + + # Check that the step source activates within the correct time range: + # For values_no_effects, the activated values are in the range [t_start, t_end] + for i, time in enumerate(times): + if t_start <= time and time <= t_end: + assert np.all(values_no_effects[i] == brightness) + else: + assert np.all(values_no_effects[i] == 0.0) + + # With redshift = 1.0, the activated values are *observed* in the range [t_start*(1+redshift), + # t_end*(1+redshift)]. Also, the values are scaled by a factor of (1+redshift). + for i, time in enumerate(times): + if t_start * (1 + redshift) <= time and time <= t_end * (1 + redshift): + assert np.all(values_redshift[i] == brightness / (1 + redshift)) + else: + assert np.all(values_redshift[i] == 0.0) + + +def test_other_redshift_values() -> None: + """Test that we can create a Redshift object with various other redshift values.""" + times = np.linspace(0, 100, 1000) + wavelengths = np.array([100.0, 200.0, 300.0]) + t_start = 10.0 + t_end = 30.0 + brightness = 50.0 + + for redshift in [0.0, 0.5, 2.0, 3.0, 30.0]: + model_redshift = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) + model_redshift.add_effect(Redshift(pz=redshift)) + values_redshift = model_redshift.evaluate(times, wavelengths) + + for i, time in enumerate(times): + if t_start * (1 + redshift) <= time and time <= t_end * (1 + redshift): + assert np.all(values_redshift[i] == brightness / (1 + redshift)) + else: + assert np.all(values_redshift[i] == 0.0) From 150f45c46c88d8a413ef7ac7c0f4b7dff653b34c Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Wed, 3 Jul 2024 11:00:31 -0400 Subject: [PATCH 02/10] Update pz to redshift --- src/tdastro/effects/redshift.py | 12 ++++++------ tests/tdastro/effects/test_redshift.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index da0fd4e8..f09048b3 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -9,7 +9,7 @@ class Redshift(EffectModel): Attributes ---------- - pz : `float` + redshift : `float` The redshift. Notes @@ -20,18 +20,18 @@ class Redshift(EffectModel): - observed_flux = emitted_flux / (1 + redshift) """ - def __init__(self, pz, **kwargs): + def __init__(self, redshift, **kwargs): """Create a Redshift effect model. Parameters ---------- - pz : `float` + redshift : `float` The redshift. **kwargs : `dict`, optional Any additional keyword arguments. """ super().__init__(**kwargs) - self.pz = pz + self.redshift = redshift def pre_effect(self, observed_times, observed_wavelengths, **kwargs): """Calculate the emitted wavelengths/times needed to give us the observed wavelengths @@ -51,7 +51,7 @@ def pre_effect(self, observed_times, observed_wavelengths, **kwargs): float The adjusted flux density at the observed wavelength. """ - return (observed_times / (1 + self.pz), observed_wavelengths / (1 + self.pz)) + return (observed_times / (1 + self.redshift), observed_wavelengths / (1 + self.redshift)) def apply(self, flux_density, bands=None, physical_model=None, **kwargs): """Apply the effect to observations (flux_density values). @@ -73,4 +73,4 @@ def apply(self, flux_density, bands=None, physical_model=None, **kwargs): flux_density : `numpy.ndarray` The results. """ - return flux_density / (1 + self.pz) + return flux_density / (1 + self.redshift) diff --git a/tests/tdastro/effects/test_redshift.py b/tests/tdastro/effects/test_redshift.py index 31da34a5..ddc6ef09 100644 --- a/tests/tdastro/effects/test_redshift.py +++ b/tests/tdastro/effects/test_redshift.py @@ -7,7 +7,7 @@ def get_no_effect_and_redshifted_values(times, wavelengths, t_start, t_end, brig """Get the values for a source with no effects and a redshifted source.""" model_no_effects = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) model_redshift = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) - model_redshift.add_effect(Redshift(pz=redshift)) + model_redshift.add_effect(Redshift(redshift=redshift)) values_no_effects = model_no_effects.evaluate(times, wavelengths) values_redshift = model_redshift.evaluate(times, wavelengths) @@ -60,7 +60,7 @@ def test_other_redshift_values() -> None: for redshift in [0.0, 0.5, 2.0, 3.0, 30.0]: model_redshift = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) - model_redshift.add_effect(Redshift(pz=redshift)) + model_redshift.add_effect(Redshift(redshift=redshift)) values_redshift = model_redshift.evaluate(times, wavelengths) for i, time in enumerate(times): From 6284779c5d4b4d60ade2373fe949c2169f9d3dca Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Tue, 9 Jul 2024 01:19:40 -0400 Subject: [PATCH 03/10] Add PR comments, plus the beginning of required parameter code I'd been adding --- src/tdastro/effects/redshift.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index f09048b3..9b3dcbcb 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -31,25 +31,28 @@ def __init__(self, redshift, **kwargs): Any additional keyword arguments. """ super().__init__(**kwargs) - self.redshift = redshift + self.required_parameters = ["redshift"] + self.redshift = redshift # TODO get this from the physical model def pre_effect(self, observed_times, observed_wavelengths, **kwargs): - """Calculate the emitted wavelengths/times needed to give us the observed wavelengths - and times given the redshift. + """Calculate the emitted times and wavelengths needed to give us the observed times and wavelengths + given the redshift. Parameters ---------- - observed_times : float + observed_times : numpy.ndarray The times at which the observation is made. - observed_wavelengths : float + observed_wavelengths : numpy.ndarray The wavelengths at which the observation is made. **kwargs : `dict`, optional Any additional keyword arguments. Returns ------- - float - The adjusted flux density at the observed wavelength. + tuple of (numpy.ndarray, numpy.ndarray) + The emission-frame times and wavelengths needed to generate the emission-frame flux densities, + which will then be redshifted to observation-frame flux densities at the observation-frame + times and wavelengths. """ return (observed_times / (1 + self.redshift), observed_wavelengths / (1 + self.redshift)) @@ -73,4 +76,6 @@ def apply(self, flux_density, bands=None, physical_model=None, **kwargs): flux_density : `numpy.ndarray` The results. """ + if physical_model is None: + raise ValueError("No physical model provided to Redshift effect.") return flux_density / (1 + self.redshift) From ea8ef0af7522be9008c93fbcd914c0c115cb7c18 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Wed, 10 Jul 2024 14:19:27 -0400 Subject: [PATCH 04/10] Update redshift to be relative to t0 and take params from given model --- src/tdastro/base_models.py | 5 ++++- src/tdastro/effects/redshift.py | 40 +++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/tdastro/base_models.py b/src/tdastro/base_models.py index 94b1ccb5..9a0cfc45 100644 --- a/src/tdastro/base_models.py +++ b/src/tdastro/base_models.py @@ -158,11 +158,13 @@ class PhysicalModel(ParameterizedModel): The object's declination (in degrees) distance : `float` The object's distance (in pc) + redshift : `float` + The object's redshift. effects : `list` A list of effects to apply to an observations. """ - def __init__(self, ra=None, dec=None, distance=None, **kwargs): + def __init__(self, ra=None, dec=None, distance=None, redshift=None, **kwargs): super().__init__(**kwargs) self.effects = [] @@ -170,6 +172,7 @@ def __init__(self, ra=None, dec=None, distance=None, **kwargs): self.add_parameter("ra", ra) self.add_parameter("dec", dec) self.add_parameter("distance", distance) + self.add_parameter("redshift", redshift) def __str__(self): """Return the string representation of the model.""" diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index 9b3dcbcb..790b458c 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -16,25 +16,37 @@ class Redshift(EffectModel): ----- Conversions used are as follows: - emitted_wavelength = observed_wavelength / (1 + redshift) - - emitted_time = observed_time / (1 + redshift) + - emitted_time = (t0 - observation_time) / (1 + redshift) + t0 - observed_flux = emitted_flux / (1 + redshift) """ - def __init__(self, redshift, **kwargs): + def __init__(self, redshift=None, t0=None, **kwargs): """Create a Redshift effect model. Parameters ---------- redshift : `float` The redshift. + t0 : `float` + The epoch of the peak or the zero phase, date. # TODO WORDING (1/?) **kwargs : `dict`, optional Any additional keyword arguments. """ super().__init__(**kwargs) - self.required_parameters = ["redshift"] - self.redshift = redshift # TODO get this from the physical model + self.add_parameter("redshift", redshift, required=True, **kwargs) + self.add_parameter("t0", t0, required=True, **kwargs) - def pre_effect(self, observed_times, observed_wavelengths, **kwargs): + def required_parameters(self): # TODO - can this just be an attribute? + """Return the required parameters for the Redshift effect model.""" + return ["redshift", "t0"] + + def __str__(self) -> str: + """Return a string representation of the Redshift effect model.""" + return f"RedshiftEffect(redshift={self.redshift})" + + def pre_effect( + self, observed_times, observed_wavelengths, **kwargs + ): # TODO WORDING (2/?) -> should I change "emitted" to "rest"? """Calculate the emitted times and wavelengths needed to give us the observed times and wavelengths given the redshift. @@ -54,20 +66,24 @@ def pre_effect(self, observed_times, observed_wavelengths, **kwargs): which will then be redshifted to observation-frame flux densities at the observation-frame times and wavelengths. """ - return (observed_times / (1 + self.redshift), observed_wavelengths / (1 + self.redshift)) + observed_times_rel_to_t0 = observed_times - self.t0 + emitted_times_rel_to_t0 = observed_times_rel_to_t0 / (1 + self.redshift) + emitted_times = emitted_times_rel_to_t0 + self.t0 + emitted_wavelengths = observed_wavelengths / (1 + self.redshift) + return (emitted_times, emitted_wavelengths) - def apply(self, flux_density, bands=None, physical_model=None, **kwargs): + def apply(self, flux_density, wavelengths=None, physical_model=None, **kwargs): """Apply the effect to observations (flux_density values). Parameters ---------- flux_density : `numpy.ndarray` - An array of flux density values. - bands : `numpy.ndarray`, optional - An array of bands. + A length T X N matrix of flux density values. + wavelengths : `numpy.ndarray`, optional + A length N array of wavelengths. physical_model : `PhysicalModel` - A PhysicalModel from which the effect may query parameters - such as redshift, position, or distance. + A PhysicalModel from which the effect may query parameters such as redshift, position, or + distance. **kwargs : `dict`, optional Any additional keyword arguments. From 67413bc0105c9a701b2e9d3ad8e6a6b2fc0a0093 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Wed, 10 Jul 2024 14:59:43 -0400 Subject: [PATCH 05/10] Convert required_parameters to attribute; tidy up redshift's apply --- src/tdastro/base_models.py | 23 +++++++++-------------- src/tdastro/effects/redshift.py | 26 +++++++++----------------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/tdastro/base_models.py b/src/tdastro/base_models.py index 9a0cfc45..35b9e38f 100644 --- a/src/tdastro/base_models.py +++ b/src/tdastro/base_models.py @@ -194,8 +194,7 @@ def add_effect(self, effect, **kwargs): Raises a ``AttributeError`` if the PhysicalModel does not have all of the required attributes. """ - required: list = effect.required_parameters() - for parameter in required: + for parameter in effect.required_parameters: # Raise an AttributeError if the parameter is missing or set to None. if getattr(self, parameter) is None: raise AttributeError(f"Parameter {parameter} unset for model {type(self).__name__}") @@ -274,26 +273,22 @@ def sample_parameters(self, include_effects=True, **kwargs): class EffectModel(ParameterizedModel): - """A physical or systematic effect to apply to an observation.""" + """A physical or systematic effect to apply to an observation. + + Attributes + ---------- + required_parameters : `list` of `str` + A list of the parameters of a PhysicalModel that this effect needs to access. + """ def __init__(self, **kwargs): super().__init__(**kwargs) + self.required_parameters = [] def __str__(self): """Return the string representation of the model.""" return "EffectModel" - def required_parameters(self): - """Returns a list of the parameters of a PhysicalModel - that this effect needs to access. - - Returns - ------- - parameters : `list` of `str` - A list of every required parameter the effect needs. - """ - return [] - def apply(self, flux_density, wavelengths=None, physical_model=None, **kwargs): """Apply the effect to observations (flux_density values) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index 790b458c..47b8c80c 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -5,12 +5,8 @@ class Redshift(EffectModel): """A redshift effect model. This contains a "pre-effect" method, which is used to calculate the emitted wavelengths/times - needed to give us the observed wavelengths and times given the redshift. - - Attributes - ---------- - redshift : `float` - The redshift. + needed to give us the observed wavelengths and times given the redshift. Times are calculated + with respect to the t0 of the given model. Notes ----- @@ -28,25 +24,23 @@ def __init__(self, redshift=None, t0=None, **kwargs): redshift : `float` The redshift. t0 : `float` - The epoch of the peak or the zero phase, date. # TODO WORDING (1/?) + The epoch of the peak or the zero phase, date. + # TODO WORDING (1/3) -> Both how this is written, and to check, are we picking t0 or epoch? **kwargs : `dict`, optional Any additional keyword arguments. """ super().__init__(**kwargs) + self.required_parameters["redshift", "t0"] self.add_parameter("redshift", redshift, required=True, **kwargs) self.add_parameter("t0", t0, required=True, **kwargs) - def required_parameters(self): # TODO - can this just be an attribute? - """Return the required parameters for the Redshift effect model.""" - return ["redshift", "t0"] - def __str__(self) -> str: """Return a string representation of the Redshift effect model.""" return f"RedshiftEffect(redshift={self.redshift})" def pre_effect( self, observed_times, observed_wavelengths, **kwargs - ): # TODO WORDING (2/?) -> should I change "emitted" to "rest"? + ): # TODO WORDING (2/3) -> Should I change "emitted" to "rest"? What is standard here? """Calculate the emitted times and wavelengths needed to give us the observed times and wavelengths given the redshift. @@ -64,7 +58,7 @@ def pre_effect( tuple of (numpy.ndarray, numpy.ndarray) The emission-frame times and wavelengths needed to generate the emission-frame flux densities, which will then be redshifted to observation-frame flux densities at the observation-frame - times and wavelengths. + times and wavelengths. # TODO WORDING (3/3) """ observed_times_rel_to_t0 = observed_times - self.t0 emitted_times_rel_to_t0 = observed_times_rel_to_t0 / (1 + self.redshift) @@ -72,7 +66,7 @@ def pre_effect( emitted_wavelengths = observed_wavelengths / (1 + self.redshift) return (emitted_times, emitted_wavelengths) - def apply(self, flux_density, wavelengths=None, physical_model=None, **kwargs): + def apply(self, flux_density, wavelengths, physical_model=None, **kwargs): """Apply the effect to observations (flux_density values). Parameters @@ -90,8 +84,6 @@ def apply(self, flux_density, wavelengths=None, physical_model=None, **kwargs): Returns ------- flux_density : `numpy.ndarray` - The results. + The redshifted results. """ - if physical_model is None: - raise ValueError("No physical model provided to Redshift effect.") return flux_density / (1 + self.redshift) From 2148ce7be0d92cd42debe9e35ae13477c08bdf71 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Wed, 10 Jul 2024 15:40:43 -0400 Subject: [PATCH 06/10] Update unit tests to use new time-interval calculation --- src/tdastro/effects/redshift.py | 2 +- tests/tdastro/effects/test_redshift.py | 35 +++++++++++++------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index 47b8c80c..6b7c8867 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -30,7 +30,7 @@ def __init__(self, redshift=None, t0=None, **kwargs): Any additional keyword arguments. """ super().__init__(**kwargs) - self.required_parameters["redshift", "t0"] + self.required_parameters = ["redshift", "t0"] self.add_parameter("redshift", redshift, required=True, **kwargs) self.add_parameter("t0", t0, required=True, **kwargs) diff --git a/tests/tdastro/effects/test_redshift.py b/tests/tdastro/effects/test_redshift.py index ddc6ef09..689dd09d 100644 --- a/tests/tdastro/effects/test_redshift.py +++ b/tests/tdastro/effects/test_redshift.py @@ -3,11 +3,11 @@ from tdastro.sources.step_source import StepSource -def get_no_effect_and_redshifted_values(times, wavelengths, t_start, t_end, brightness, redshift) -> tuple: +def get_no_effect_and_redshifted_values(times, wavelengths, t0, t1, brightness, redshift) -> tuple: """Get the values for a source with no effects and a redshifted source.""" - model_no_effects = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) - model_redshift = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) - model_redshift.add_effect(Redshift(redshift=redshift)) + model_no_effects = StepSource(brightness=brightness, t0=t0, t1=t1) + model_redshift = StepSource(brightness=brightness, t0=t0, t1=t1, redshift=redshift) + model_redshift.add_effect(Redshift(redshift=model_redshift, t0=model_redshift)) values_no_effects = model_no_effects.evaluate(times, wavelengths) values_redshift = model_redshift.evaluate(times, wavelengths) @@ -23,28 +23,29 @@ def test_redshift() -> None: """Test that we can create a Redshift object and it gives us values as expected.""" times = np.array([1, 2, 3, 5, 10]) wavelengths = np.array([100.0, 200.0, 300.0]) - t_start = 1.0 - t_end = 2.0 + t0 = 1.0 + t1 = 2.0 brightness = 15.0 redshift = 1.0 # Get the values for a redshifted step source, and a step source with no effects for comparison (values_no_effects, values_redshift) = get_no_effect_and_redshifted_values( - times, wavelengths, t_start, t_end, brightness, redshift + times, wavelengths, t0, t1, brightness, redshift ) # Check that the step source activates within the correct time range: - # For values_no_effects, the activated values are in the range [t_start, t_end] + # For values_no_effects, the activated values are in the range [t0, t1] for i, time in enumerate(times): - if t_start <= time and time <= t_end: + if t0 <= time and time <= t1: assert np.all(values_no_effects[i] == brightness) else: assert np.all(values_no_effects[i] == 0.0) - # With redshift = 1.0, the activated values are *observed* in the range [t_start*(1+redshift), - # t_end*(1+redshift)]. Also, the values are scaled by a factor of (1+redshift). + # With redshift = 1.0, the activated values are *observed* in the range [(t0-t0)*(1+redshift)+t0, + # (t1-t0*(1+redshift+t0] (the first term reduces to t0). Also, the values are scaled by a factor + # of (1+redshift). for i, time in enumerate(times): - if t_start * (1 + redshift) <= time and time <= t_end * (1 + redshift): + if t0 <= time and time <= (t1 - t0) * (1 + redshift) + t0: assert np.all(values_redshift[i] == brightness / (1 + redshift)) else: assert np.all(values_redshift[i] == 0.0) @@ -54,17 +55,17 @@ def test_other_redshift_values() -> None: """Test that we can create a Redshift object with various other redshift values.""" times = np.linspace(0, 100, 1000) wavelengths = np.array([100.0, 200.0, 300.0]) - t_start = 10.0 - t_end = 30.0 + t0 = 10.0 + t1 = 30.0 brightness = 50.0 for redshift in [0.0, 0.5, 2.0, 3.0, 30.0]: - model_redshift = StepSource(brightness=brightness, t_start=t_start, t_end=t_end) - model_redshift.add_effect(Redshift(redshift=redshift)) + model_redshift = StepSource(brightness=brightness, t0=t0, t1=t1, redshift=redshift) + model_redshift.add_effect(Redshift(redshift=model_redshift, t0=model_redshift)) values_redshift = model_redshift.evaluate(times, wavelengths) for i, time in enumerate(times): - if t_start * (1 + redshift) <= time and time <= t_end * (1 + redshift): + if t0 <= time and time <= (t1 - t0) * (1 + redshift) + t0: assert np.all(values_redshift[i] == brightness / (1 + redshift)) else: assert np.all(values_redshift[i] == 0.0) From 761b57eef4e778e7fa6df25bffd2c34e8cb5f5e6 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Tue, 16 Jul 2024 00:56:02 -0400 Subject: [PATCH 07/10] Incorporate comments on nomenclature; fix docstring equation --- src/tdastro/effects/redshift.py | 38 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index 6b7c8867..0fbf076c 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -11,9 +11,9 @@ class Redshift(EffectModel): Notes ----- Conversions used are as follows: - - emitted_wavelength = observed_wavelength / (1 + redshift) - - emitted_time = (t0 - observation_time) / (1 + redshift) + t0 - - observed_flux = emitted_flux / (1 + redshift) + - rest_frame_wavelength = observation_frame_wavelength / (1 + redshift) + - rest_frame_time = (observation_frame_time - t0) / (1 + redshift) + t0 + - observation_frame_flux = rest_frame_flux / (1 + redshift) """ def __init__(self, redshift=None, t0=None, **kwargs): @@ -24,8 +24,8 @@ def __init__(self, redshift=None, t0=None, **kwargs): redshift : `float` The redshift. t0 : `float` - The epoch of the peak or the zero phase, date. - # TODO WORDING (1/3) -> Both how this is written, and to check, are we picking t0 or epoch? + The reference epoch; e.g. the time of the maximum light of a supernova or the epoch of zero phase + for a periodic variable star. **kwargs : `dict`, optional Any additional keyword arguments. """ @@ -38,17 +38,15 @@ def __str__(self) -> str: """Return a string representation of the Redshift effect model.""" return f"RedshiftEffect(redshift={self.redshift})" - def pre_effect( - self, observed_times, observed_wavelengths, **kwargs - ): # TODO WORDING (2/3) -> Should I change "emitted" to "rest"? What is standard here? - """Calculate the emitted times and wavelengths needed to give us the observed times and wavelengths - given the redshift. + def pre_effect(self, observer_frame_times, observer_frame_wavelengths, **kwargs): + """Calculate the rest-frame times and wavelengths needed to give us the observer-frame times + and wavelengths (given the redshift). Parameters ---------- - observed_times : numpy.ndarray + observer_frame_times : numpy.ndarray The times at which the observation is made. - observed_wavelengths : numpy.ndarray + observer_frame_wavelengths : numpy.ndarray The wavelengths at which the observation is made. **kwargs : `dict`, optional Any additional keyword arguments. @@ -56,15 +54,15 @@ def pre_effect( Returns ------- tuple of (numpy.ndarray, numpy.ndarray) - The emission-frame times and wavelengths needed to generate the emission-frame flux densities, - which will then be redshifted to observation-frame flux densities at the observation-frame - times and wavelengths. # TODO WORDING (3/3) + The rest-frame times and wavelengths needed to generate the rest-frame flux densities, + which will later be redshifted back to observer-frame flux densities at the observer-frame + times and wavelengths. """ - observed_times_rel_to_t0 = observed_times - self.t0 - emitted_times_rel_to_t0 = observed_times_rel_to_t0 / (1 + self.redshift) - emitted_times = emitted_times_rel_to_t0 + self.t0 - emitted_wavelengths = observed_wavelengths / (1 + self.redshift) - return (emitted_times, emitted_wavelengths) + observed_times_rel_to_t0 = observer_frame_times - self.t0 + rest_frame_times_rel_to_t0 = observed_times_rel_to_t0 / (1 + self.redshift) + rest_frame_times = rest_frame_times_rel_to_t0 + self.t0 + rest_frame_wavelengths = observer_frame_wavelengths / (1 + self.redshift) + return (rest_frame_times, rest_frame_wavelengths) def apply(self, flux_density, wavelengths, physical_model=None, **kwargs): """Apply the effect to observations (flux_density values). From 24d5f56a155667826ca4752b56c21659c9894a61 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Tue, 16 Jul 2024 14:35:08 -0400 Subject: [PATCH 08/10] Add back pre-effects to moved PhysicalModel --- src/tdastro/sources/physical_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tdastro/sources/physical_model.py b/src/tdastro/sources/physical_model.py index b790b8a9..c40a9919 100644 --- a/src/tdastro/sources/physical_model.py +++ b/src/tdastro/sources/physical_model.py @@ -129,6 +129,11 @@ def evaluate(self, times, wavelengths, resample_parameters=False, **kwargs): if resample_parameters: self.sample_parameters(kwargs) + # Pre-effects are adjustments done to times and/or wavelengths, before flux density computation. + for effect in self.effects: + if hasattr(effect, "pre_effect"): + times, wavelengths = effect.pre_effect(times, wavelengths, **kwargs) + # Compute the flux density for both the current object and add in anything # behind it, such as a host galaxy. flux_density = self._evaluate(times, wavelengths, **kwargs) From b982a95348474fc260ab9caaf55d933b9a2fb4c2 Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Tue, 16 Jul 2024 14:44:57 -0400 Subject: [PATCH 09/10] Remove EffectModel's required_parameters, as new strategy calls add_parameter with required=True --- src/tdastro/base_models.py | 2 +- src/tdastro/effects/effect_model.py | 11 ----------- src/tdastro/effects/redshift.py | 1 - src/tdastro/sources/physical_model.py | 6 ------ 4 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/tdastro/base_models.py b/src/tdastro/base_models.py index 5a288004..4cc15c84 100644 --- a/src/tdastro/base_models.py +++ b/src/tdastro/base_models.py @@ -322,4 +322,4 @@ def compute(self, **kwargs): args[key] = kwargs[key] else: args[key] = getattr(self, key) - return self.func(**args) \ No newline at end of file + return self.func(**args) diff --git a/src/tdastro/effects/effect_model.py b/src/tdastro/effects/effect_model.py index 83885bda..9be7101c 100644 --- a/src/tdastro/effects/effect_model.py +++ b/src/tdastro/effects/effect_model.py @@ -9,17 +9,6 @@ class EffectModel(ParameterizedNode): def __init__(self, **kwargs): super().__init__(**kwargs) - def required_parameters(self): - """Returns a list of the parameters of a PhysicalModel - that this effect needs to access. - - Returns - ------- - parameters : `list` of `str` - A list of every required parameter the effect needs. - """ - return [] - def apply(self, flux_density, wavelengths=None, physical_model=None, **kwargs): """Apply the effect to observations (flux_density values) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index 0fbf076c..f15d5f8b 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -30,7 +30,6 @@ def __init__(self, redshift=None, t0=None, **kwargs): Any additional keyword arguments. """ super().__init__(**kwargs) - self.required_parameters = ["redshift", "t0"] self.add_parameter("redshift", redshift, required=True, **kwargs) self.add_parameter("t0", t0, required=True, **kwargs) diff --git a/src/tdastro/sources/physical_model.py b/src/tdastro/sources/physical_model.py index c40a9919..8698042b 100644 --- a/src/tdastro/sources/physical_model.py +++ b/src/tdastro/sources/physical_model.py @@ -79,12 +79,6 @@ def add_effect(self, effect, allow_dups=True, **kwargs): if effect_type == type(prev): raise ValueError("Added the effect type to a model {effect_type} more than once.") - required: list = effect.required_parameters() - for parameter in required: - # Raise an AttributeError if the parameter is missing or set to None. - if getattr(self, parameter) is None: - raise AttributeError(f"Parameter {parameter} unset for model {type(self).__name__}") - self.effects.append(effect) def _evaluate(self, times, wavelengths, **kwargs): From 580ca5f3b496d8ce62c606ce71e14db878871b1d Mon Sep 17 00:00:00 2001 From: Olivia Lynn Date: Tue, 16 Jul 2024 15:18:39 -0400 Subject: [PATCH 10/10] Update import path --- src/tdastro/effects/redshift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tdastro/effects/redshift.py b/src/tdastro/effects/redshift.py index f15d5f8b..de594f57 100644 --- a/src/tdastro/effects/redshift.py +++ b/src/tdastro/effects/redshift.py @@ -1,4 +1,4 @@ -from tdastro.base_models import EffectModel +from tdastro.effects.effect_model import EffectModel class Redshift(EffectModel):