From fefe0e5dacaf883e4776d3a2481f5b8eb39011d1 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 5 Jul 2023 14:31:06 -0500 Subject: [PATCH 001/144] Adding wavex model --- src/pint/models/wavex.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/pint/models/wavex.py diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py new file mode 100644 index 000000000..7db9650c1 --- /dev/null +++ b/src/pint/models/wavex.py @@ -0,0 +1,42 @@ +"""Delays expressed as a sum of sinusoids.""" +import astropy.units as u +import numpy as np + +from pint.models.parameter import MJDParameter, floatParameter, prefixParameter +from pint.models.timing_model import DelayComponent, MissingParameter + +class WaveX(DelayComponent): +# These lines are in wave.py and not sure what they are meant to do +# register = True +# category = "wave" + def __init__(self): + super().__init__() + self.add_param( + MJDParameter( + name="WXEPOCH", + description="Reference epoch for wave delay solution", + time_scale="tdb", + ) + ) + self.add_param( + prefixParameter( + name="WXFREQ_", + description="Base frequency of wave delay solution", + units="1/d", + ) + ) + self.add_param( + prefixParameter( + name="WXSIN_", + description="Sine amplitudes for wave delay function", + units="s", + ) + ) + self.add_param( + prefixParameter( + name="WXCOS_", + description="Cosine amplitudes for wave delay function", + units="s", + ) + ) + #self.delay_funcs_component += [self.wavex_delay] \ No newline at end of file From 28ff9a01bd410348f513b3e9744559b7b1db64ab Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 5 Jul 2023 16:15:34 -0500 Subject: [PATCH 002/144] add wavex_delay --- src/pint/models/wavex.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 7db9650c1..496c3785c 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -39,4 +39,32 @@ def __init__(self): units="s", ) ) - #self.delay_funcs_component += [self.wavex_delay] \ No newline at end of file + #self.delay_funcs_component += [self.wavex_delay] + + #Initialize setup + # def setup(self): + # super().setup() + # self.wave_freqs = list(self.get_prefix_mapping_component("WXFREQ_").keys()) + # self.num_wave_freqs = len(self.wave_freqs) + + #Placeholder for validation tests + # def validate(self) + + def wavex_delay(self,toas,delays): + total_delay = 0 + # wave_freq_params = self.get_prefix_mapping_component("WXFREQ_") + wave_freqs = self.get_prefix_mapping_component("WXFREQ_").values() + wave_sins = self.get_prefix_mapping_component("WXSIN_").values() + wave_cos = self.get_prefix_mapping_component("WXSIN_").values() + base_phase = ( + toas.table["tbdld"] * u.day + - self.WXEPOCH.value * u.day + - delays.to(u.day) + ).value + for f,freq in enumerate(wave_freqs): + arg = 2. * np.pi * freq.value * base_phase + total_delay += (wave_sins[f] * np.sin(arg) + wave_cos[f] * np.cos(arg)) + + + + From 06e2b6aaf13f482f4aa933e9eb676def1ef6abbd Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 6 Jul 2023 08:19:53 -0500 Subject: [PATCH 003/144] commit changes --- src/pint/models/wavex.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 496c3785c..d01eef802 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -65,6 +65,9 @@ def wavex_delay(self,toas,delays): arg = 2. * np.pi * freq.value * base_phase total_delay += (wave_sins[f] * np.sin(arg) + wave_cos[f] * np.cos(arg)) + #Placeholder for calculations of derivatives + # def d_wavex_delay + From 71c0b0cce30aaca3a272523ca47561bb42683404 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 6 Jul 2023 20:59:52 -0500 Subject: [PATCH 004/144] uncomment register --- src/pint/models/wavex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index d01eef802..ee7ef1010 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -7,13 +7,13 @@ class WaveX(DelayComponent): # These lines are in wave.py and not sure what they are meant to do -# register = True + register = True # category = "wave" def __init__(self): super().__init__() self.add_param( MJDParameter( - name="WXEPOCH", + name= "WXEPOCH", description="Reference epoch for wave delay solution", time_scale="tdb", ) From 3a2424d15461cebd885c8b577b59a08c948b623b Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 6 Jul 2023 22:08:15 -0500 Subject: [PATCH 005/144] added comments --- src/pint/models/wavex.py | 72 +++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index ee7ef1010..9fe70587d 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -5,69 +5,79 @@ from pint.models.parameter import MJDParameter, floatParameter, prefixParameter from pint.models.timing_model import DelayComponent, MissingParameter + class WaveX(DelayComponent): -# These lines are in wave.py and not sure what they are meant to do + """Implementation of the wave model as a delay correction + Delays are expressed as a sum of sinusoids. + Used for decomposition of timing noise into a series of sine/cosine components + with the amplitudes as fitted parameters. + + + Parameters supported: + + .. paramtable:: + :class: pint.models.wave.Wave + + This is an extension of the L13 method described in Lentati et al., 2013 + """ + register = True -# category = "wave" + + # category = "wave" def __init__(self): super().__init__() self.add_param( MJDParameter( - name= "WXEPOCH", + name="WXEPOCH", description="Reference epoch for wave delay solution", time_scale="tdb", ) ) self.add_param( prefixParameter( - name="WXFREQ_", - description="Base frequency of wave delay solution", - units="1/d", + name="WXFREQ_", + description="Base frequency of wave delay solution", + units="1/d", ) ) self.add_param( prefixParameter( - name="WXSIN_", - description="Sine amplitudes for wave delay function", - units="s", + name="WXSIN_", + description="Sine amplitudes for wave delay function", + units="s", ) ) self.add_param( prefixParameter( - name="WXCOS_", - description="Cosine amplitudes for wave delay function", - units="s", + name="WXCOS_", + description="Cosine amplitudes for wave delay function", + units="s", ) ) - #self.delay_funcs_component += [self.wavex_delay] + # self.delay_funcs_component += [self.wavex_delay] + + # Initialize setup + def setup(self): + super().setup() - #Initialize setup - # def setup(self): - # super().setup() # self.wave_freqs = list(self.get_prefix_mapping_component("WXFREQ_").keys()) # self.num_wave_freqs = len(self.wave_freqs) - - #Placeholder for validation tests + + # Placeholder for validation tests # def validate(self) - def wavex_delay(self,toas,delays): + def wavex_delay(self, toas, delays): total_delay = 0 - # wave_freq_params = self.get_prefix_mapping_component("WXFREQ_") + # wave_freq_params = self.get_prefix_mapping_component("WXFREQ_") wave_freqs = self.get_prefix_mapping_component("WXFREQ_").values() wave_sins = self.get_prefix_mapping_component("WXSIN_").values() wave_cos = self.get_prefix_mapping_component("WXSIN_").values() base_phase = ( - toas.table["tbdld"] * u.day - - self.WXEPOCH.value * u.day - - delays.to(u.day) + toas.table["tbdld"] * u.day - self.WXEPOCH.value * u.day - delays.to(u.day) ).value - for f,freq in enumerate(wave_freqs): - arg = 2. * np.pi * freq.value * base_phase - total_delay += (wave_sins[f] * np.sin(arg) + wave_cos[f] * np.cos(arg)) + for f, freq in enumerate(wave_freqs): + arg = 2.0 * np.pi * freq.value * base_phase + total_delay += wave_sins[f] * np.sin(arg) + wave_cos[f] * np.cos(arg) - #Placeholder for calculations of derivatives + # Placeholder for calculations of derivatives # def d_wavex_delay - - - - From fb4854c8adefeb8f0389886f593e200eb7ef440b Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 6 Jul 2023 22:38:21 -0500 Subject: [PATCH 006/144] comments added --- src/pint/models/wavex.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 9fe70587d..9226b25ac 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -8,7 +8,9 @@ class WaveX(DelayComponent): """Implementation of the wave model as a delay correction + Delays are expressed as a sum of sinusoids. + Used for decomposition of timing noise into a series of sine/cosine components with the amplitudes as fitted parameters. From 4ca631d116cbad4c9aa360c621f79e6e19057250 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 11:24:32 -0500 Subject: [PATCH 007/144] started add_wavex_components --- src/pint/models/wavex.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 9226b25ac..0085c2ed7 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -11,8 +11,7 @@ class WaveX(DelayComponent): Delays are expressed as a sum of sinusoids. - Used for decomposition of timing noise into a series of sine/cosine components - with the amplitudes as fitted parameters. + Used for decomposition of timing noise into a series of sine/cosine components with the amplitudes as fitted parameters. Parameters supported: @@ -35,28 +34,52 @@ def __init__(self): time_scale="tdb", ) ) + self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) + self.delay_funcs_component += [self.wavex_delay] + + def add_wavex_components(self, index=None, frozens=True): + """Add WaveX components + + Parameters + ---------- + + index : int, None + Interger label for WaveX component. If None, will increment largest used index by 1. + frozens : iterable of bool or bool + Indicates whether wavex will be fit + + Returns + ------- + + """ + + #### If index is None, increment the current max WaveX index by 1. Increment using WXFREQ + if index is None: + dct = self.get_prefix_mapping_component("WXFREQ_") + index = np.max(list(dct.keys())) + 1 + i = f"{int(index):04d}" + self.add_param( prefixParameter( - name="WXFREQ_", + name="WXFREQ_{i}", description="Base frequency of wave delay solution", units="1/d", ) ) self.add_param( prefixParameter( - name="WXSIN_", + name="WXSIN_{i}", description="Sine amplitudes for wave delay function", units="s", ) ) self.add_param( prefixParameter( - name="WXCOS_", + name="WXCOS_{i}", description="Cosine amplitudes for wave delay function", units="s", ) ) - # self.delay_funcs_component += [self.wavex_delay] # Initialize setup def setup(self): From 91edc28015512b03e6bac1113b3fe66c3b48e75f Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 12:31:50 -0500 Subject: [PATCH 008/144] add wavex component --- src/pint/models/wavex.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 0085c2ed7..a78b6b18b 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -37,12 +37,14 @@ def __init__(self): self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) self.delay_funcs_component += [self.wavex_delay] - def add_wavex_components(self, index=None, frozens=True): - """Add WaveX components + def add_wavex_component(self, wxfreq=1, index=None, frozens=True): + """Add WaveX component Parameters ---------- + wxfreq : float or astropy.quantity.Quantity + Base frequency for WaveX component index : int, None Interger label for WaveX component. If None, will increment largest used index by 1. frozens : iterable of bool or bool @@ -51,6 +53,8 @@ def add_wavex_components(self, index=None, frozens=True): Returns ------- + index : int + Index that has been assigned to new WaveX component """ #### If index is None, increment the current max WaveX index by 1. Increment using WXFREQ @@ -59,11 +63,20 @@ def add_wavex_components(self, index=None, frozens=True): index = np.max(list(dct.keys())) + 1 i = f"{int(index):04d}" + if int(index) in self.get_prefix_mapping_component("WXFREQ_"): + raise ValueError( + f"Index '{index}' is already in use in this model. Please choose another" + ) + + if isinstance(wxfreq, u.quantity.Quantity): + wxfreq = wxfreq.to_value(u.d**-1) self.add_param( prefixParameter( name="WXFREQ_{i}", description="Base frequency of wave delay solution", units="1/d", + value=wxfreq, + frozen=frozen, ) ) self.add_param( @@ -80,6 +93,9 @@ def add_wavex_components(self, index=None, frozens=True): units="s", ) ) + self.setup() + self.validate() + return index # Initialize setup def setup(self): From e4731f2be0b4a3ffdc4757223831ecdce2f4465e Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 14:31:27 -0500 Subject: [PATCH 009/144] fixed add_wavex_component(s) --- src/pint/models/wavex.py | 130 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index a78b6b18b..8aca1e412 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -1,6 +1,7 @@ """Delays expressed as a sum of sinusoids.""" import astropy.units as u import numpy as np +from loguru import logger as log from pint.models.parameter import MJDParameter, floatParameter, prefixParameter from pint.models.timing_model import DelayComponent, MissingParameter @@ -37,7 +38,7 @@ def __init__(self): self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) self.delay_funcs_component += [self.wavex_delay] - def add_wavex_component(self, wxfreq=1, index=None, frozens=True): + def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True): """Add WaveX component Parameters @@ -47,7 +48,11 @@ def add_wavex_component(self, wxfreq=1, index=None, frozens=True): Base frequency for WaveX component index : int, None Interger label for WaveX component. If None, will increment largest used index by 1. - frozens : iterable of bool or bool + wxsin : float or astropy.quantity.Quantity + Sine amplitude for WaveX component + wxcos : float or astropy.quantity.Quantity + Cosine amplitude for WaveX component + frozen : iterable of bool or bool Indicates whether wavex will be fit Returns @@ -68,15 +73,19 @@ def add_wavex_component(self, wxfreq=1, index=None, frozens=True): f"Index '{index}' is already in use in this model. Please choose another" ) + if isinstance(wxsin, u.quantity.Quantity): + wxsin = wxsin.to_value(u.s) + if isinstance(wxcos, u.quantity.Quantity): + wxcos = wxcos.to_value(u.s) if isinstance(wxfreq, u.quantity.Quantity): - wxfreq = wxfreq.to_value(u.d**-1) + # wxfreq = wxfreq.value + wxfreq = wxfreq.to_value(1 / u.d) self.add_param( prefixParameter( name="WXFREQ_{i}", description="Base frequency of wave delay solution", units="1/d", value=wxfreq, - frozen=frozen, ) ) self.add_param( @@ -84,6 +93,8 @@ def add_wavex_component(self, wxfreq=1, index=None, frozens=True): name="WXSIN_{i}", description="Sine amplitudes for wave delay function", units="s", + value=wxsin, + frozen=frozen, ) ) self.add_param( @@ -91,12 +102,123 @@ def add_wavex_component(self, wxfreq=1, index=None, frozens=True): name="WXCOS_{i}", description="Cosine amplitudes for wave delay function", units="s", + value=wxcos, + frozen=frozen, ) ) self.setup() self.validate() return index + def add_wavex_components( + self, wxfreqs, indices=None, wxsins=0, wxcoses=0, frozens=True + ): + """Add WaveX components with specified base frequencies + + Parameters + ---------- + + wxfreqs : iterable of float or astropy.quantity.Quantity + Base frequencies for WaveX components + indices : iterable of int, None + Interger labels for WaveX components. If None, will increment largest used index by 1. + wxsins : iterable of float or astropy.quantity.Quantity + Sine amplitudes for WaveX components + wxcoses : iterable of float or astropy.quantity.Quantity + Cosine amplitudes for WaveX components + frozens : iterable of bool or bool + Indicates whether sine adn cosine amplitudes of wavex components will be fit + Returns + ------- + + indices : list + Indices that have been assigned to new WaveX components + """ + + if indices is None: + indices = [None] * len(wxfreqs) + wxsins = np.atleast_1d(wxsins) + wxcoses = np.atleast_1d(wxcoses) + if len(wxsins) == 1: + wxsins = np.repeat(wxsins, len(wxfreqs)) + if len(wxcoses) == 1: + wxcoses = np.repeat(wxcoses, len(wxfreqs)) + if len(wxsins) != len(wxfreqs): + raise ValueError( + f"Number of base frequencies {len(wxfreqs)} doesn't match number of sine ampltudes {len(wxsins)}" + ) + if len(wxcoses) != len(wxfreqs): + raise ValueError( + f"Number of base frequencies {len(wxfreqs)} doesn't match number of cosine ampltudes {len(wxcoses)}" + ) + frozens = np.atleast_1d(frozens) + if len(frozens) == 1: + frozens = np.repeat(frozens, len(wxfreqs)) + if len(frozens) != len(wxfreqs): + raise ValueError( + f"Number of base frequencies must match number of frozen values" + ) + #### If indices is None, increment the current max WaveX index by 1. Increment using WXFREQ + dct = self.get_prefix_mapping_component("WXFREQ_") + last_index = np.max(list(dct.keys())) + added_indices = [] + for wxfreq, index, wxsin, wxcos, frozen in zip( + wxfreqs, indices, wxsins, wxcoses, frozens + ): + if index is None: + index = last_index + 1 + last_index += 1 + elif index in list(dct.keys()): + raise ValueError( + f"Attempting to insert WXFREQ_{index:04d} but it already exists" + ) + added_indices.append(index) + i = f"{int(index):04d}" + + if int(index) in dct: + raise ValueError( + f"Index '{index}' is already in use in this model. Please choose another" + ) + if isinstance(wxfreq, u.quantity.Quantity): + wxfreq = wxfreq.to_value(u.d**-1) + if isinstance(wxsin, u.quantity.Quantity): + wxsin = wxsin.to_value(u.s) + if isinstance(wxcos, u.quantity.Quantity): + wxcos = wxcos.to_value(u.s) + log.trace(f"Adding WXSIN_{i} and WXCOS_{i} at frequency WXFREQ_{i}") + self.add_param( + prefixParameter( + name="WXFREQ_{i}", + description="Base frequency of wave delay solution", + units="1/d", + value=wxfreq, + parameter_type="float", + ) + ) + self.add_param( + prefixParameter( + name="WXSIN_{i}", + description="Sine amplitudes for wave delay function", + units="s", + value=wxsin, + parameter_type="float", + frozen=frozen, + ) + ) + self.add_param( + prefixParameter( + name="WXCOS_{i}", + description="Cosine amplitudes for wave delay function", + units="s", + value=wxcos, + parameter_type="float", + frozen=frozen, + ) + ) + self.setup() + self.validate() + return added_indices + # Initialize setup def setup(self): super().setup() From d023865fefa50e55205230a87988b325559e4e70 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 14:44:52 -0500 Subject: [PATCH 010/144] added remove_wavex_component --- src/pint/models/wavex.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 8aca1e412..823716294 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -219,6 +219,29 @@ def add_wavex_components( self.validate() return added_indices + def remove_wavex_component(self, index): + """Remove all WaveX components associated with a given index or list of indices + + Parameters + ---------- + index : float, int, list, np.ndarray + Number or list/array of numbers corresponding to WaveX indices to be removed from model. + """ + + if isinstance(index, (int, float, np.int64)): + indices = [index] + elif isinstance(index, (list, set, np.ndarray)): + indices = index + else: + raise TypeError( + f"index most be a float, int, set, list, or array - not {type(index)}" + ) + for index in indices: + index_rf = f"{int(index):04d}" + for prefix in ["WXFREQ_", "WXSIN_", "WXCOS_"]: + self.remove_param(prefix + index_rf) + self.validate() + # Initialize setup def setup(self): super().setup() From 8c7d58d5c1326c9333de3f11690ce73510502520 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 14:57:11 -0500 Subject: [PATCH 011/144] add get_indices --- src/pint/models/wavex.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 823716294..e927bfc2f 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -242,6 +242,17 @@ def remove_wavex_component(self, index): self.remove_param(prefix + index_rf) self.validate() + def get_indices(self): + """Returns an array of intergers corresponding to WaveX component parameters using WXFREQs + + Returns + ------- + inds : np.ndarray + Array of WaveX indices in model. + """ + inds = [int(p.split("_")[-1]) for p in self.params if "WXFREQ_" in p] + return np.array(inds) + # Initialize setup def setup(self): super().setup() From df0159316867df558e59cdb974b93b8e335418e6 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 15:25:24 -0500 Subject: [PATCH 012/144] Add validate and setup --- src/pint/models/wavex.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index e927bfc2f..eac18c254 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -14,11 +14,10 @@ class WaveX(DelayComponent): Used for decomposition of timing noise into a series of sine/cosine components with the amplitudes as fitted parameters. - Parameters supported: .. paramtable:: - :class: pint.models.wave.Wave + :class: pint.models.wavex.WaveX This is an extension of the L13 method described in Lentati et al., 2013 """ @@ -257,11 +256,34 @@ def get_indices(self): def setup(self): super().setup() + # Get WaveX mapping + # Register WXSIN and WXCOS derivatives PLACEHOLDER + # for prefix_par in self.get_params_of_type("prefixParameter"): + # if prefix_par.startswith("WXSIN_"): + # self.register_deriv_funcs() + # if prefix_par.startswith("WXCOS_"): + # self.register_deriv_funcs() # self.wave_freqs = list(self.get_prefix_mapping_component("WXFREQ_").keys()) # self.num_wave_freqs = len(self.wave_freqs) - # Placeholder for validation tests - # def validate(self) + # PLACEHOLDER for validation tests + def validate(self): + """Validate all the WaveX parameters""" + super().validate() + WXFREQ_mapping = self.get_prefix_mapping_component("WXFREQ_") + WXSIN_mapping = self.get_prefix_mapping_component("WXSIN_") + WXCOS_mapping = self.get_prefix_mapping_component("WXCOS_") + if WXFREQ_mapping.keys() != WXSIN_mapping.keys(): + # PLACEHOLDER : Report the mismatched parameters + raise ValueError( + "WXFREQ_ parameters do not match WXSIN_ parameters." + "Please check your prefixed parameters" + ) + if WXFREQ_mapping.keys() != WXCOS_mapping.keys(): + raise ValueError( + "WXFREQ_ parameters do not match WXCOS_ parameters." + "Please check your prefixed parameters" + ) def wavex_delay(self, toas, delays): total_delay = 0 From bb82654742ece9a70d790996a2fede614483b71f Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 7 Jul 2023 16:00:51 -0500 Subject: [PATCH 013/144] fixed delay function --- src/pint/models/wavex.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index eac18c254..31158867e 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -266,10 +266,10 @@ def setup(self): # self.wave_freqs = list(self.get_prefix_mapping_component("WXFREQ_").keys()) # self.num_wave_freqs = len(self.wave_freqs) - # PLACEHOLDER for validation tests def validate(self): """Validate all the WaveX parameters""" super().validate() + self.setup() WXFREQ_mapping = self.get_prefix_mapping_component("WXFREQ_") WXSIN_mapping = self.get_prefix_mapping_component("WXSIN_") WXCOS_mapping = self.get_prefix_mapping_component("WXCOS_") @@ -284,6 +284,13 @@ def validate(self): "WXFREQ_ parameters do not match WXCOS_ parameters." "Please check your prefixed parameters" ) + if self.WXEPOCH.quantity is None: + if self.PEPOCH.quantity is None: + raise MissingParameter( + "WXEPOCH or PEPOCH are required if WaveX is being used" + ) + else: + self.WXEPOCH = self.PEPOCH def wavex_delay(self, toas, delays): total_delay = 0 @@ -296,7 +303,10 @@ def wavex_delay(self, toas, delays): ).value for f, freq in enumerate(wave_freqs): arg = 2.0 * np.pi * freq.value * base_phase - total_delay += wave_sins[f] * np.sin(arg) + wave_cos[f] * np.cos(arg) + total_delay += wave_sins[f].quantity * np.sin(arg) + wave_cos[f] * np.cos( + arg + ) + return total_delay # Placeholder for calculations of derivatives # def d_wavex_delay From ba1ec7261b589befa2e3ffe3a75321f11454da99 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 10 Jul 2023 12:19:42 -0500 Subject: [PATCH 014/144] add inital component --- src/pint/models/wavex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 31158867e..2ace5ee06 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -34,6 +34,7 @@ def __init__(self): time_scale="tdb", ) ) + self.add_wavex_component(None, index=1, wxsin=0, wxcos=0, frozen=False) self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) self.delay_funcs_component += [self.wavex_delay] From c07e8dff838033180d4753541cea97777f9aec59 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 10 Jul 2023 12:31:48 -0500 Subject: [PATCH 015/144] registering new class --- src/pint/models/__init__.py | 1 + src/pint/models/timing_model.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pint/models/__init__.py b/src/pint/models/__init__.py index ca7900338..7c846a007 100644 --- a/src/pint/models/__init__.py +++ b/src/pint/models/__init__.py @@ -41,6 +41,7 @@ from pint.models.timing_model import DEFAULT_ORDER, TimingModel from pint.models.troposphere_delay import TroposphereDelay from pint.models.wave import Wave +from pint.models.wavex import WaveX # Define a standard basic model StandardTimingModel = TimingModel( diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 4951a58c5..22f1db127 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -117,6 +117,7 @@ "spindown", "phase_jump", "wave", + "wavex" ] From 5311f2cff648e17a5121aa75e826daf08d6790a8 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 10 Jul 2023 12:32:32 -0500 Subject: [PATCH 016/144] added category --- src/pint/models/wavex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 2ace5ee06..63bc64f57 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -23,8 +23,8 @@ class WaveX(DelayComponent): """ register = True + category = "wavex" - # category = "wave" def __init__(self): super().__init__() self.add_param( From 6fd886e95a8becc391e82503defbefd89f910bc2 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 10 Jul 2023 12:36:32 -0500 Subject: [PATCH 017/144] add comma --- src/pint/models/timing_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 22f1db127..58164a9cc 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -117,7 +117,7 @@ "spindown", "phase_jump", "wave", - "wavex" + "wavex", ] From d9232349c53a0880e467d7d133a4c1a44e7735fd Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 11 Jul 2023 08:29:19 -0500 Subject: [PATCH 018/144] Fixed wavex_delay --- src/pint/models/wavex.py | 55 ++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 63bc64f57..9e253d2e0 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -82,28 +82,31 @@ def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True) wxfreq = wxfreq.to_value(1 / u.d) self.add_param( prefixParameter( - name="WXFREQ_{i}", + name=f"WXFREQ_{i}", description="Base frequency of wave delay solution", units="1/d", value=wxfreq, + parameter_type="float", ) ) self.add_param( prefixParameter( - name="WXSIN_{i}", + name=f"WXSIN_{i}", description="Sine amplitudes for wave delay function", units="s", value=wxsin, frozen=frozen, + parameter_type="float", ) ) self.add_param( prefixParameter( - name="WXCOS_{i}", + name=f"WXCOS_{i}", description="Cosine amplitudes for wave delay function", units="s", value=wxcos, frozen=frozen, + parameter_type="float", ) ) self.setup() @@ -188,7 +191,7 @@ def add_wavex_components( log.trace(f"Adding WXSIN_{i} and WXCOS_{i} at frequency WXFREQ_{i}") self.add_param( prefixParameter( - name="WXFREQ_{i}", + name=f"WXFREQ_{i}", description="Base frequency of wave delay solution", units="1/d", value=wxfreq, @@ -197,7 +200,7 @@ def add_wavex_components( ) self.add_param( prefixParameter( - name="WXSIN_{i}", + name=f"WXSIN_{i}", description="Sine amplitudes for wave delay function", units="s", value=wxsin, @@ -207,7 +210,7 @@ def add_wavex_components( ) self.add_param( prefixParameter( - name="WXCOS_{i}", + name=f"WXCOS_{i}", description="Cosine amplitudes for wave delay function", units="s", value=wxcos, @@ -285,28 +288,30 @@ def validate(self): "WXFREQ_ parameters do not match WXCOS_ parameters." "Please check your prefixed parameters" ) - if self.WXEPOCH.quantity is None: - if self.PEPOCH.quantity is None: - raise MissingParameter( - "WXEPOCH or PEPOCH are required if WaveX is being used" - ) - else: - self.WXEPOCH = self.PEPOCH + if self.WXEPOCH.value is None: + if self._parent is not None: + if self._parent.PEPOCH.value is None: + raise MissingParameter( + "WXEPOCH or PEPOCH are required if WaveX is being used" + ) + else: + self.WXEPOCH = self._parent.PEPOCH def wavex_delay(self, toas, delays): - total_delay = 0 - # wave_freq_params = self.get_prefix_mapping_component("WXFREQ_") - wave_freqs = self.get_prefix_mapping_component("WXFREQ_").values() - wave_sins = self.get_prefix_mapping_component("WXSIN_").values() - wave_cos = self.get_prefix_mapping_component("WXSIN_").values() + total_delay = np.zeros(toas.ntoas) * u.s + wave_freqs = self.get_prefix_mapping_component("WXFREQ_") + wave_sins = self.get_prefix_mapping_component("WXSIN_") + wave_cos = self.get_prefix_mapping_component("WXCOS_") + base_phase = ( - toas.table["tbdld"] * u.day - self.WXEPOCH.value * u.day - delays.to(u.day) - ).value - for f, freq in enumerate(wave_freqs): - arg = 2.0 * np.pi * freq.value * base_phase - total_delay += wave_sins[f].quantity * np.sin(arg) + wave_cos[f] * np.cos( - arg - ) + toas.table["tdbld"].value * u.d - self.WXEPOCH.value * u.d - delays.to(u.d) + ) + for idx, param in wave_freqs.items(): + freq = getattr(self, param).quantity + wxsin = getattr(self, wave_sins[idx]).quantity + wxcos = getattr(self, wave_cos[idx]).quantity + arg = 2.0 * np.pi * freq * base_phase + total_delay += wxsin * np.sin(arg.value) + wxcos * np.cos(arg.value) return total_delay # Placeholder for calculations of derivatives From 1778ca4882733964eec3ef6ed347f712becf423b Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 11 Jul 2023 10:51:01 -0500 Subject: [PATCH 019/144] fix validate --- src/pint/models/wavex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 9e253d2e0..070968564 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -295,7 +295,7 @@ def validate(self): "WXEPOCH or PEPOCH are required if WaveX is being used" ) else: - self.WXEPOCH = self._parent.PEPOCH + self.WXEPOCH.quantity = self._parent.PEPOCH.quantity def wavex_delay(self, toas, delays): total_delay = np.zeros(toas.ntoas) * u.s From 8cb296fb296019aea22091fbc40d1b7a9e9d3472 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 11 Jul 2023 13:47:51 -0500 Subject: [PATCH 020/144] Add validate test and start tests --- src/pint/models/wavex.py | 21 ++++++++++++++++++ tests/test_wavex.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/test_wavex.py diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 070968564..cc744cc04 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -283,11 +283,32 @@ def validate(self): "WXFREQ_ parameters do not match WXSIN_ parameters." "Please check your prefixed parameters" ) + if WXFREQ_mapping.keys() != WXCOS_mapping.keys(): raise ValueError( "WXFREQ_ parameters do not match WXCOS_ parameters." "Please check your prefixed parameters" ) + # if len(WXFREQ_mapping.keys()) != len(WXSIN_mapping.keys()): + # raise ValueError( + # "The number of WXFREQ_ parameters do not match the number of WXSIN_ parameters." + # "Please check your prefixed parameters" + # ) + # if len(WXFREQ_mapping.keys()) != len(WXCOS_mapping.keys()): + # raise ValueError( + # "The number of WXFREQ_ parameters do not match the number of WXCOS_ parameters." + # "Please check your prefixed parameters" + # ) + if WXSIN_mapping.keys() != WXCOS_mapping.keys(): + raise ValueError( + "WXSIN_ parameters do not match WXCOS_ parameters." + "Please check your prefixed parameters" + ) + # if len(WXSIN_mapping.keys()) != len(WXCOS_mapping.keys()): + # raise ValueError( + # "The number of WXSIN_ and WXCOS_ parameters do not match" + # "Please check your prefixed parameters" + # ) if self.WXEPOCH.value is None: if self._parent is not None: if self._parent.PEPOCH.value is None: diff --git a/tests/test_wavex.py b/tests/test_wavex.py new file mode 100644 index 000000000..24d6b0470 --- /dev/null +++ b/tests/test_wavex.py @@ -0,0 +1,47 @@ +from io import StringIO +import pytest +import numpy as np + +from astropy import units as u +from pint.models import get_model, get_model_and_toas +from pint.fitter import Fitter +from pint.toa import get_TOAs +from pint.simulation import make_fake_toas_uniform +import pint.utils +from pinttestdata import datadir + +par1 = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB + """ + +# Introduce a par file with WaveX already present + +par2 = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + WXEPOCH 55321.000000 + WXFREQ_0001 0.1 + WXSIN_0001 1 + WXCOS_0001 1 + UNITS TDB + """ From c42b0509721f3b8d452d8e28e9f23b33990dceba Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 17 Jul 2023 16:20:51 -0500 Subject: [PATCH 021/144] Adding test --- tests/test_wavex.py | 61 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 24d6b0470..db454b9e1 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -4,6 +4,7 @@ from astropy import units as u from pint.models import get_model, get_model_and_toas +from pint.models.timing_model import Component from pint.fitter import Fitter from pint.toa import get_TOAs from pint.simulation import make_fake_toas_uniform @@ -45,3 +46,63 @@ WXCOS_0001 1 UNITS TDB """ + +wavex_par = """ + WXFREQ_0002 0.2 + WXSIN_0002 2 + WXCOS_0002 2 + WXFREQ_0003 0.3 + WXSIN_0003 3 + WXCOS_0003 3 +""" + + +def test_wavex_from_par(): + # Check that a par file with wavex components present produces expected indices + model = get_model(StringIO(par2 + wavex_par)) + indices = model.components["WaveX"].get_indices() + assert np.all(np.array(indices) == np.array([1, 2, 3])) + + +def test_add_wavex_to_par(): + # Add a wavex component to par file that has none and check against par file with some WaveX model + model = get_model(StringIO(par1)) + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + model.add_component(Component.component_types["WaveX"]()) + index = model.components["WaveX"].get_indices() + model.components["WaveX"].WXFREQ_0001.quantity = 0.1 * (1 / u.d) + model.components["WaveX"].WXSIN_0001.quantity = 1 * u.s + model.components["WaveX"].WXCOS_0001.quantity = 1 * u.s + wavex_model = get_model(StringIO(par2)) + assert np.all( + np.array(index) == np.array(wavex_model.components["WaveX"].get_indices()) + ) + assert np.all( + model.components["WaveX"].wavex_delay(toas, 0.0 * u.s) + == wavex_model.components["WaveX"].wavex_delay(toas, 0.0 * u.s) + ) + + +def test_multiple_wavexs(): + # Check that when adding multiple wavex component pythonically is consistent with a par file with the same components + model = get_model(StringIO(par2)) + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + wavex_model = get_model(StringIO(par2 + wavex_par)) + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], indices=[2, 3], wxsins=[2, 3], wxcoses=[2, 3] + ) + assert np.all(np.array(indices) == np.array([2, 3])) + assert np.all( + model.components["WaveX"].wavex_delay(toas, 0.0 * u.s) + == wavex_model.components["WaveX"].wavex_delay(toas, 0.0 * u.s) + ) + + +def test_add_then_remove_wavex(): + # Check that adding and then removing a wavex component actually gets rid of it + model = get_model(StringIO(par2)) + model.components["WaveX"].add_wavex_component(0.2, index=2, wxsin=2, wxcos=2) + indices = model.components["WaveX"].get_indices() + model.components["WaveX"].remove_wavex_component(2) + index = model.components["WaveX"].get_indices() + assert np.all(np.array(len(indices)) != np.array(len(index))) From 518d60d91415087f7c0aca0deef80871506f3aef Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 17 Jul 2023 16:26:50 -0500 Subject: [PATCH 022/144] Validate that num wxsin = num wxcos --- src/pint/models/wavex.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index cc744cc04..eaf362730 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -304,11 +304,11 @@ def validate(self): "WXSIN_ parameters do not match WXCOS_ parameters." "Please check your prefixed parameters" ) - # if len(WXSIN_mapping.keys()) != len(WXCOS_mapping.keys()): - # raise ValueError( - # "The number of WXSIN_ and WXCOS_ parameters do not match" - # "Please check your prefixed parameters" - # ) + if len(WXSIN_mapping.keys()) != len(WXCOS_mapping.keys()): + raise ValueError( + "The number of WXSIN_ and WXCOS_ parameters do not match" + "Please check your prefixed parameters" + ) if self.WXEPOCH.value is None: if self._parent is not None: if self._parent.PEPOCH.value is None: From 26a8e85c26498051703664d28db30cfa6eac2cba Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 18 Jul 2023 10:58:01 -0500 Subject: [PATCH 023/144] Add warnings to validate --- src/pint/models/wavex.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index eaf362730..e29f17ecd 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -2,6 +2,7 @@ import astropy.units as u import numpy as np from loguru import logger as log +from warnings import warn from pint.models.parameter import MJDParameter, floatParameter, prefixParameter from pint.models.timing_model import DelayComponent, MissingParameter @@ -34,7 +35,7 @@ def __init__(self): time_scale="tdb", ) ) - self.add_wavex_component(None, index=1, wxsin=0, wxcos=0, frozen=False) + self.add_wavex_component(0.1, index=1, wxsin=0, wxcos=0, frozen=False) self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) self.delay_funcs_component += [self.wavex_delay] @@ -283,7 +284,6 @@ def validate(self): "WXFREQ_ parameters do not match WXSIN_ parameters." "Please check your prefixed parameters" ) - if WXFREQ_mapping.keys() != WXCOS_mapping.keys(): raise ValueError( "WXFREQ_ parameters do not match WXCOS_ parameters." @@ -309,6 +309,20 @@ def validate(self): "The number of WXSIN_ and WXCOS_ parameters do not match" "Please check your prefixed parameters" ) + wfreqs = np.zeros(len(WXFREQ_mapping)) + for j, index in enumerate(WXFREQ_mapping): + if (getattr(self, f"WXFREQ_{index:04d}").value == 0) or ( + getattr(self, f"WXFREQ_{index:04d}").quantity is None + ): + raise ValueError( + f"WXFREQ_{index:04d} is zero or None. Please check your prefixed parameters" + ) + if getattr(self, f"WXFREQ_{index:04d}").value < 0.0: + warn(f"Frequency WXFREQ_{index:04d} is negative") + wfreqs[j] = getattr(self, f"WXFREQ_{index:04d}").value + wfreqs.sort() + if np.any(np.diff(wfreqs) <= (1.0 / (2.0 * 364.25))): + warn("Frequency resolution is greater than 1/yr") if self.WXEPOCH.value is None: if self._parent is not None: if self._parent.PEPOCH.value is None: From 20449079ac9a8e38a67e794532b6c48c2bfd1860 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 18 Jul 2023 11:37:49 -0500 Subject: [PATCH 024/144] Adding tests --- tests/test_wavex.py | 89 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index db454b9e1..4f1b45bb8 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -83,7 +83,17 @@ def test_add_wavex_to_par(): ) -def test_multiple_wavexs(): +def test_add_then_remove_wavex(): + # Check that adding and then removing a wavex component actually gets rid of it + model = get_model(StringIO(par2)) + model.components["WaveX"].add_wavex_component(0.2, index=2, wxsin=2, wxcos=2) + indices = model.components["WaveX"].get_indices() + model.components["WaveX"].remove_wavex_component(2) + index = model.components["WaveX"].get_indices() + assert np.all(np.array(len(indices)) != np.array(len(index))) + + +def test_multiple_wavex(): # Check that when adding multiple wavex component pythonically is consistent with a par file with the same components model = get_model(StringIO(par2)) toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") @@ -98,11 +108,74 @@ def test_multiple_wavexs(): ) -def test_add_then_remove_wavex(): - # Check that adding and then removing a wavex component actually gets rid of it +def test_multiple_wavex_broadcast_frozens(): + # Check that when a single False is given for frozens, it gets broadcast to all the sine and cosine amplitudes model = get_model(StringIO(par2)) - model.components["WaveX"].add_wavex_component(0.2, index=2, wxsin=2, wxcos=2) - indices = model.components["WaveX"].get_indices() - model.components["WaveX"].remove_wavex_component(2) - index = model.components["WaveX"].get_indices() - assert np.all(np.array(len(indices)) != np.array(len(index))) + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], + indices=[2, 3], + wxsins=[2, 3], + wxcoses=[2, 3], + frozens=False, + ) + for index in indices: + assert getattr(model, f"WXSIN_{index:04d}").frozen == False + assert getattr(model, f"WXCOS_{index:04d}").frozen == False + + +def test_multiple_wavex_wrong_cos_amps(): + # Check that code breaks when adding an extra cosine amplitude than there are frequencies, indices, and sine amplitudes for + model = get_model(StringIO(par2)) + with pytest.raises(ValueError): + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], indices=[2, 3], wxsins=[2, 3], wxcoses=[2, 3, 4] + ) + + +def test_multiple_wavex_wrong_sin_amps(): + # Check that code breaks when adding an extra sine amplitude than there are frequencies, indices, and cosine amplitudes for + model = get_model(StringIO(par2)) + with pytest.raises(ValueError): + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], indices=[2, 3], wxsins=[2, 3, 4], wxcoses=[2, 3] + ) + + +def test_multiple_wavex_wrong_freqs(): + # Check that code breaks when not adding enough frequencies for the number of indices, sine amps, and cosine amps given + model = get_model(StringIO(par2)) + with pytest.raises(ValueError): + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], indices=[2, 3, 4], wxsins=[2, 3, 4], wxcoses=[2, 3, 4] + ) + + +def test_multiple_wavex_wrong_frozens(): + # Check that adding to many elements to frozens breaks code + model = get_model(StringIO(par2)) + with pytest.raises(ValueError): + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], + indices=[2, 3], + wxsins=[2, 3], + wxcoses=[2, 3], + frozens=[False, False, False], + ) + + +def test_multiple_wavex_explicit_indices(): + # Check that adding specific indices is done correctly + model = get_model(StringIO(par2)) + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], indices=[3, 4], wxsins=[2, 3], wxcoses=[2, 3] + ) + assert np.all(np.array(indices) == np.array([3, 4])) + + +def test_multiple_wavex_explicit_indices_duplicate(): + # Check that adding a duplicate index fails + model = get_model(StringIO(par2)) + with pytest.raises(ValueError): + indices = model.components["WaveX"].add_wavex_components( + [0.2, 0.3], indices=[1, 3], wxsins=[2, 3], wxcoses=[2, 3] + ) From c0051065c08bd37fe191aa518235076ab4fcc791 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 18 Jul 2023 16:07:24 -0500 Subject: [PATCH 025/144] Additional tests --- tests/test_wavex.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 4f1b45bb8..cc1c4d174 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -6,6 +6,7 @@ from pint.models import get_model, get_model_and_toas from pint.models.timing_model import Component from pint.fitter import Fitter +from pint.residuals import Residuals from pint.toa import get_TOAs from pint.simulation import make_fake_toas_uniform import pint.utils @@ -57,6 +58,47 @@ """ +def wavex_delay(waves, toas): + total_delay = np.zeros(toas.ntoas) * u.s + wave_freqs = waves.get_prefix_mapping_component("WXFREQ_") + wave_sins = waves.get_prefix_mapping_component("WXSIN_") + wave_cos = waves.get_prefix_mapping_component("WXCOS_") + + base_phase = toas.table["tdbld"].value * u.d - waves.WXEPOCH.value * u.d + for idx, param in wave_freqs.items(): + freq = getattr(waves, param).quantity + wxsin = getattr(waves, wave_sins[idx]).quantity + wxcos = getattr(waves, wave_cos[idx]).quantity + arg = 2.0 * np.pi * freq * base_phase + total_delay += wxsin * np.sin(arg.value) + wxcos * np.cos(arg.value) + return total_delay + + +def test_wavex_resids_amp(): + # Check that the amplitude of residuals somewhat matches independent calculation of wave delay for a single component + model = get_model(StringIO(par1)) + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + wave_model = get_model(StringIO(par2)) + rs = Residuals(toas, wave_model) + injected_amp = np.sqrt( + wave_model.components["WaveX"].WXSIN_0001.value ** 2 + + wave_model.components["WaveX"].WXCOS_0001.value ** 2 + ) + print(injected_amp, max(rs.resids.value)) + assert np.isclose(max(rs.resids.value), injected_amp, atol=1e-2) + assert np.isclose(min(rs.resids.value), -injected_amp, atol=1e-2) + + +def test_multiple_wavex_resids_amp(): + # Check that residuals for multiple components match independent calculation + model = get_model(StringIO(par1)) + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + wave_model = get_model(StringIO(par2 + wavex_par)) + rs = Residuals(toas, wave_model) + wave_delays = wavex_delay(wave_model.components["WaveX"], toas) + assert np.allclose(rs.resids, -wave_delays, atol=max(rs.resids.value) / 10.0) + + def test_wavex_from_par(): # Check that a par file with wavex components present produces expected indices model = get_model(StringIO(par2 + wavex_par)) From a633b2c400dd3a616da484f15bc79dda0cd37417 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 18 Jul 2023 16:10:58 -0500 Subject: [PATCH 026/144] Small fixes to tests --- tests/test_wavex.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index cc1c4d174..8885bcf83 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -81,12 +81,11 @@ def test_wavex_resids_amp(): wave_model = get_model(StringIO(par2)) rs = Residuals(toas, wave_model) injected_amp = np.sqrt( - wave_model.components["WaveX"].WXSIN_0001.value ** 2 - + wave_model.components["WaveX"].WXCOS_0001.value ** 2 + wave_model.components["WaveX"].WXSIN_0001.quantity ** 2 + + wave_model.components["WaveX"].WXCOS_0001.quantity ** 2 ) - print(injected_amp, max(rs.resids.value)) - assert np.isclose(max(rs.resids.value), injected_amp, atol=1e-2) - assert np.isclose(min(rs.resids.value), -injected_amp, atol=1e-2) + assert np.isclose(max(rs.resids), injected_amp, atol=1e-2) + assert np.isclose(min(rs.resids), -injected_amp, atol=1e-2) def test_multiple_wavex_resids_amp(): From cdc4583c6d9623825cc53f88bb96ea5f27c15bad Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 19 Jul 2023 14:24:17 -0500 Subject: [PATCH 027/144] Added amplitude fitting --- src/pint/models/wavex.py | 39 +++++++++++++++++++++++++++++---------- tests/test_wavex.py | 4 ++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index e29f17ecd..5ad6e4be5 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -39,6 +39,8 @@ def __init__(self): self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) self.delay_funcs_component += [self.wavex_delay] + # Register derivative functions PLACEHOLDER + def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True): """Add WaveX component @@ -261,15 +263,15 @@ def get_indices(self): def setup(self): super().setup() - # Get WaveX mapping - # Register WXSIN and WXCOS derivatives PLACEHOLDER - # for prefix_par in self.get_params_of_type("prefixParameter"): - # if prefix_par.startswith("WXSIN_"): - # self.register_deriv_funcs() - # if prefix_par.startswith("WXCOS_"): - # self.register_deriv_funcs() - # self.wave_freqs = list(self.get_prefix_mapping_component("WXFREQ_").keys()) - # self.num_wave_freqs = len(self.wave_freqs) + # Get WaveX mapping + # Register WXSIN and WXCOS derivatives PLACEHOLDER + for prefix_par in self.get_params_of_type("prefixParameter"): + if prefix_par.startswith("WXSIN_"): + self.register_deriv_funcs(self.d_wavex_delay_d_WXSIN, prefix_par) + if prefix_par.startswith("WXCOS_"): + self.register_deriv_funcs(self.d_wavex_delay_d_WXCOS, prefix_par) + self.wave_freqs = list(self.get_prefix_mapping_component("WXFREQ_").keys()) + self.num_wave_freqs = len(self.wave_freqs) def validate(self): """Validate all the WaveX parameters""" @@ -332,6 +334,9 @@ def validate(self): else: self.WXEPOCH.quantity = self._parent.PEPOCH.quantity + def validate_toas(self, toas): + return super().validate_toas(toas) + def wavex_delay(self, toas, delays): total_delay = np.zeros(toas.ntoas) * u.s wave_freqs = self.get_prefix_mapping_component("WXFREQ_") @@ -350,4 +355,18 @@ def wavex_delay(self, toas, delays): return total_delay # Placeholder for calculations of derivatives - # def d_wavex_delay + def d_wavex_delay_d_WXSIN(self, toas, param, delays, acc_delay=None): + par = getattr(self, param) + freq = getattr(self, f"WXFREQ_{int(par.index):04d}").quantity + base_phase = toas.table["tdbld"].value * u.d - self.WXEPOCH.value * u.d + arg = 2.0 * np.pi * freq * base_phase + deriv = np.sin(arg.value) + return deriv * u.s / par.units + + def d_wavex_delay_d_WXCOS(self, toas, param, delays, acc_delay=None): + par = getattr(self, param) + freq = getattr(self, f"WXFREQ_{int(par.index):04d}").quantity + base_phase = toas.table["tdbld"].value * u.d - self.WXEPOCH.value * u.d + arg = 2.0 * np.pi * freq * base_phase + deriv = np.cos(arg.value) + return deriv * u.s / par.units diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 8885bcf83..00e7e0443 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -77,7 +77,7 @@ def wavex_delay(waves, toas): def test_wavex_resids_amp(): # Check that the amplitude of residuals somewhat matches independent calculation of wave delay for a single component model = get_model(StringIO(par1)) - toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + toas = make_fake_toas_uniform(55000, 55100, 500, model, obs="gbt") wave_model = get_model(StringIO(par2)) rs = Residuals(toas, wave_model) injected_amp = np.sqrt( @@ -91,7 +91,7 @@ def test_wavex_resids_amp(): def test_multiple_wavex_resids_amp(): # Check that residuals for multiple components match independent calculation model = get_model(StringIO(par1)) - toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + toas = make_fake_toas_uniform(55000, 55100, 500, model, obs="gbt") wave_model = get_model(StringIO(par2 + wavex_par)) rs = Residuals(toas, wave_model) wave_delays = wavex_delay(wave_model.components["WaveX"], toas) From 9cd23d1664c6fb7fb1abceb5bc6a9e66d225549c Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 27 Jul 2023 12:30:50 -0400 Subject: [PATCH 028/144] fixed base_phase --- src/pint/models/wavex.py | 2 +- tests/test_wavex.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 5ad6e4be5..b89bff556 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -344,7 +344,7 @@ def wavex_delay(self, toas, delays): wave_cos = self.get_prefix_mapping_component("WXCOS_") base_phase = ( - toas.table["tdbld"].value * u.d - self.WXEPOCH.value * u.d - delays.to(u.d) + toas.table["tdbld"].data * u.d - self.WXEPOCH.value * u.d - delays.to(u.d) ) for idx, param in wave_freqs.items(): freq = getattr(self, param).quantity diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 00e7e0443..7061a51ac 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -63,8 +63,7 @@ def wavex_delay(waves, toas): wave_freqs = waves.get_prefix_mapping_component("WXFREQ_") wave_sins = waves.get_prefix_mapping_component("WXSIN_") wave_cos = waves.get_prefix_mapping_component("WXCOS_") - - base_phase = toas.table["tdbld"].value * u.d - waves.WXEPOCH.value * u.d + base_phase = toas.table["tdbld"].data * u.d - waves.WXEPOCH.value * u.d for idx, param in wave_freqs.items(): freq = getattr(waves, param).quantity wxsin = getattr(waves, wave_sins[idx]).quantity @@ -74,6 +73,13 @@ def wavex_delay(waves, toas): return total_delay +# def test_derivative(): +# assert None + +# def test_fitter(): +# assert + + def test_wavex_resids_amp(): # Check that the amplitude of residuals somewhat matches independent calculation of wave delay for a single component model = get_model(StringIO(par1)) From e738efb736411e26b36905d2bdd5b332cef486db Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 11:06:54 -0500 Subject: [PATCH 029/144] normalize design matrix --- src/pint/models/timing_model.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index a7916da85..95035356f 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -1807,7 +1807,9 @@ def d_dm_d_param(self, data, param): ) return result - def designmatrix(self, toas, acc_delay=None, incfrozen=False, incoffset=True): + def designmatrix( + self, toas, acc_delay=None, incfrozen=False, incoffset=True, normalize=False + ): """Return the design matrix. The design matrix is the matrix with columns of ``d_phase_d_param/F0`` @@ -1886,6 +1888,19 @@ def designmatrix(self, toas, acc_delay=None, incfrozen=False, incoffset=True): the_unit = u.Unit("") / getattr(self, param).units M[:, ii] = q.to_value(the_unit) / F0.value units.append(the_unit / F0.unit) + + if normalize: + norm = np.sqrt(np.sum(M**2, axis=0)) + + if np.any(norm == 0): + offending_params = [params[i] for i in np.where(norm == 0)[0]] + warn( + f"Parameter degeneracy found in design matrix! The offending parameters are {offending_params}." + ) + + norm[norm == 0] = 1 + M /= norm + return M, params, units def compare( From 4d0ed02284c0564d7ecc5d1c85aef321ae15c99d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 11:10:28 -0500 Subject: [PATCH 030/144] return norm --- src/pint/models/timing_model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 95035356f..d6872bcb4 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -1889,7 +1889,9 @@ def designmatrix( M[:, ii] = q.to_value(the_unit) / F0.value units.append(the_unit / F0.unit) - if normalize: + if not normalize: + return M, params, units + else: norm = np.sqrt(np.sum(M**2, axis=0)) if np.any(norm == 0): @@ -1901,7 +1903,7 @@ def designmatrix( norm[norm == 0] = 1 M /= norm - return M, params, units + return M, norm, params, units def compare( self, From 1d2f6c224692b07e08d56dfac50390dbea2bc360 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 11:14:35 -0500 Subject: [PATCH 031/144] design matrix norm --- src/pint/fitter.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 844d15db9..529a4acb0 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -716,9 +716,11 @@ def update_resids(self): def make_resids(self, model): return Residuals(toas=self.toas, model=model, track_mode=self.track_mode) - def get_designmatrix(self): + def get_designmatrix(self, normalize=False): """Return the model's design matrix for these TOAs.""" - return self.model.designmatrix(toas=self.toas, incfrozen=False, incoffset=True) + return self.model.designmatrix( + toas=self.toas, incfrozen=False, incoffset=True, normalize=normalize + ) def _get_corr_cov_matrix( self, matrix_type, with_phase, pretty_print, prec, usecolor @@ -2165,7 +2167,13 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): fitperrs = self.model.get_params_dict("free", "uncertainty") # Define the linear system - M, params, units = self.get_designmatrix() + # normalize the design matrix + M, norm, params, units = self.get_designmatrix(normalize=True) + + self.fac = norm + # M /= norm + + ntmpar = len(fitp) # Get residuals and TOA uncertainties in seconds if i == 0: @@ -2182,19 +2190,6 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): phiinv = np.concatenate((phiinv, 1 / phi)) M = np.hstack((M, Mn)) - # normalize the design matrix - norm = np.sqrt(np.sum(M**2, axis=0)) - ntmpar = len(fitp) - for c in np.where(norm == 0)[0]: - warn( - f"Parameter degeneracy; the following parameter yields " - f"almost no change: {params[c]}", - DegeneracyWarning, - ) - norm[norm == 0] = 1 - self.fac = norm - M /= norm - # compute covariance matrices if full_cov: cov = self.model.toa_covariance_matrix(self.toas) From 7471a66b11f3b8a5fbdb0c2c3027669385ced5cd Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 14:32:34 -0500 Subject: [PATCH 032/144] normalize_designmatrix --- src/pint/fitter.py | 47 ++++++++++------------------------------------ src/pint/utils.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 844d15db9..bae611b62 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -83,7 +83,7 @@ ) from pint.residuals import Residuals, WidebandTOAResiduals from pint.toa import TOAs -from pint.utils import FTest +from pint.utils import FTest, normalize_designmatrix __all__ = [ @@ -1303,9 +1303,7 @@ def step(self): # NOTE, We remove subtract mean value here, since it did not give us a # fast converge fitting. # M[:,1:] -= M[:,1:].mean(axis=0) - fac = np.sqrt((M**2).mean(axis=0)) - fac[fac == 0] = 1.0 - M /= fac + M, fac = normalize_designmatrix(M, params) # Singular value decomp of design matrix: # M = U s V^T # Dimensions: @@ -1457,15 +1455,7 @@ def step(self): M = np.hstack((M, Mn)) # normalize the design matrix - norm = np.sqrt(np.sum(M**2, axis=0)) - for c in np.where(norm == 0)[0]: - warn( - f"Parameter degeneracy; the following parameter yields " - f"almost no change: {params[c]}", - DegeneracyWarning, - ) - norm[norm == 0] = 1 - M /= norm + M, norm = normalize_designmatrix(M, params) self.M = M self.fac = norm @@ -2014,10 +2004,7 @@ def fit_toas(self, maxiter=1, threshold=None, debug=False): # NOTE, We remove subtract mean value here, since it did not give us a # fast converge fitting. # M[:,1:] -= M[:,1:].mean(axis=0) - fac = np.sqrt((M**2).mean(axis=0)) - # fac[0] = 1.0 - fac[fac == 0] = 1.0 - M /= fac + M, fac = normalize_designmatrix(M, params) # Singular value decomp of design matrix: # M = U s V^T # Dimensions: @@ -2182,18 +2169,11 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): phiinv = np.concatenate((phiinv, 1 / phi)) M = np.hstack((M, Mn)) - # normalize the design matrix - norm = np.sqrt(np.sum(M**2, axis=0)) ntmpar = len(fitp) - for c in np.where(norm == 0)[0]: - warn( - f"Parameter degeneracy; the following parameter yields " - f"almost no change: {params[c]}", - DegeneracyWarning, - ) - norm[norm == 0] = 1 + + # normalize the design matrix + M, norm = normalize_designmatrix(M, params) self.fac = norm - M /= norm # compute covariance matrices if full_cov: @@ -2537,17 +2517,10 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): new_d_matrix.param_units, ) - # normalize the design matrix - norm = np.sqrt(np.sum(M**2, axis=0)) ntmpar = len(fitp) - for c in np.where(norm == 0)[0]: - warn( - f"Parameter degeneracy; the following parameter yields " - f"almost no change: {params[c]}", - DegeneracyWarning, - ) - norm[norm == 0] = 1 - M /= norm + + # normalize the design matrix + M, norm = normalize_designmatrix(M, params) self.fac = norm # compute covariance matrices diff --git a/src/pint/utils.py b/src/pint/utils.py index 283a7472b..e58b90f87 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -39,6 +39,7 @@ import textwrap from contextlib import contextmanager from pathlib import Path +from warnings import warn import uncertainties import astropy.constants as const @@ -2129,3 +2130,20 @@ def get_unit(parname): ac = AllComponents() return ac.param_to_unit(parname) + + +def normalize_designmatrix(M, params): + from pint.fitter import DegeneracyWarning + + norm = np.sqrt(np.sum(M**2, axis=0)) + + bad_params = [params[i] for i in np.where(norm == 0)[0]] + if len(bad_params) > 0: + warn( + f"Parameter degeneracy found in designmatrix! The offending parameters are {bad_params}.", + DegeneracyWarning, + ) + norm[norm == 0] = 1 + M1 = M / norm + + return M1, norm From 3fb6f499802b04ef1f677f913c2f9c7400d2e737 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 14:52:55 -0500 Subject: [PATCH 033/144] fix tests --- tests/test_downhill_fitter.py | 5 +---- tests/test_fitter_error_checking.py | 11 +++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/test_downhill_fitter.py b/tests/test_downhill_fitter.py index 9a79bf3ab..9574daf34 100644 --- a/tests/test_downhill_fitter.py +++ b/tests/test_downhill_fitter.py @@ -267,10 +267,7 @@ def test_degenerate_parameters_gls(model_eccentric_toas_ecorr, full_cov): f = pint.fitter.DownhillGLSFitter(toas, model_wrong) - with pytest.warns( - pint.fitter.DegeneracyWarning, - match=r".*degeneracy.*following parameter.*ELONG\b", - ): + with pytest.warns(pint.fitter.DegeneracyWarning): f.fit_toas(full_cov=full_cov, threshold=1e-14) assert abs(f.model.ECC.value - model_eccentric.ECC.value) < 1e-4 diff --git a/tests/test_fitter_error_checking.py b/tests/test_fitter_error_checking.py index 53b59241b..bcdc7f3ac 100644 --- a/tests/test_fitter_error_checking.py +++ b/tests/test_fitter_error_checking.py @@ -94,12 +94,14 @@ def test_dmx_barycentered(Fitter): toas = make_fake_toas_uniform(58000, 58900, 10, model, obs="@", freq=np.inf) model.free_params = ["F0", "DM", "DMX_0001"] fitter = Fitter(toas, model) - with pytest.warns(pint.fitter.DegeneracyWarning, match=r".*degeneracy.*DM\b"): + with pytest.warns(pint.fitter.DegeneracyWarning, match=r".*degeneracy.*DM*\b"): fitter.fit_toas() for p in fitter.model.free_params: assert not np.isnan(fitter.model[p].value) fitter = Fitter(toas, model) - with pytest.warns(pint.fitter.DegeneracyWarning, match=r".*degeneracy.*DMX_0001\b"): + with pytest.warns( + pint.fitter.DegeneracyWarning, match=r".*degeneracy.*DMX_0001*\b" + ): fitter.fit_toas() for p in fitter.model.free_params: assert not np.isnan(fitter.model[p].value) @@ -185,10 +187,7 @@ def test_null_vector(Fitter): wideband_dm_error=1e-4 * u.pc * u.cm**-3, ) fitter = Fitter(toas, model) - with pytest.warns( - pint.fitter.DegeneracyWarning, - match=r".*degeneracy.*following parameter.*ELONG\b", - ): + with pytest.warns(pint.fitter.DegeneracyWarning): fitter.fit_toas(threshold=1e-14) for p in fitter.model.free_params: assert not np.isnan(fitter.model[p].value) From b08ac83764a8538671b34487c97ad1ff1df36944 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 16:46:51 -0500 Subject: [PATCH 034/144] don't use fitter object for gls chi2 computation in residuals --- src/pint/residuals.py | 67 +++++++++++++++++++++++++++++++------------ src/pint/utils.py | 49 ++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/pint/residuals.py b/src/pint/residuals.py index 65ad45ef0..aec1645c6 100644 --- a/src/pint/residuals.py +++ b/src/pint/residuals.py @@ -12,12 +12,13 @@ import astropy.units as u import numpy as np -from scipy.linalg import LinAlgError +import scipy +from scipy.linalg import LinAlgError, cho_factor, cho_solve from loguru import logger as log from pint.models.dispersion_model import Dispersion from pint.phase import Phase -from pint.utils import weighted_mean, taylor_horner_deriv +from pint.utils import normalize_designmatrix, weighted_mean, taylor_horner_deriv __all__ = [ "Residuals", @@ -499,7 +500,51 @@ def calc_time_resids( ) return (phase_resids / self.get_PSR_freq(calctype=calctype)).to(u.s) - def calc_chi2(self, full_cov=False): + def _calc_gls_chi2(self): + self.update() + + residuals = self.time_resids.to(u.s).value + + M = self.model.noise_model_designmatrix(self.toas) + phi = self.model.noise_model_basis_weight(self.toas) + phiinv = 1 / phi + + # For implicit offset subtraction + if "PHOFF" not in self.model: + M = np.append(M, np.ones((len(self.toas), 1)), axis=1) + phiinv = np.append(phiinv, [0]) + + M, norm = normalize_designmatrix(M, None) + + phiinv /= norm**2 + Nvec = self.model.scaled_toa_uncertainty(self.toas).to(u.s).value ** 2 + cinv = 1 / Nvec + mtcm = np.dot(M.T, cinv[:, None] * M) + mtcm += np.diag(phiinv) + mtcy = np.dot(M.T, cinv * residuals) + + try: + c = cho_factor(mtcm) + xhat = cho_solve(c, mtcy) + except LinAlgError as e: + log.warning( + f"Degenerate conditions encountered when " f"computing chi-squared: {e}" + ) + + U, s, Vt = scipy.linalg.svd(mtcm, full_matrices=False) + + bad = np.where(s <= 0)[0] + s[bad] = np.inf + + xhat = np.dot(Vt.T, np.dot(U.T, mtcy) / s) + + newres = residuals - np.dot(M, xhat) + + chi2 = np.dot(newres, cinv * newres) + np.dot(xhat, phiinv * xhat) + + return chi2 + + def calc_chi2(self): """Return the weighted chi-squared for the model and toas. If the errors on the TOAs are independent this is a straightforward @@ -519,21 +564,7 @@ def calc_chi2(self, full_cov=False): correctly return infinity. """ if self.model.has_correlated_errors: - log.trace("Using GLS fitter to compute residual chi2") - # Use GLS but don't actually fit - from pint.fitter import GLSFitter - - m = copy.deepcopy(self.model) - m.free_params = [] - f = GLSFitter(self.toas, m, residuals=self) - try: - return f.fit_toas(maxiter=1, full_cov=full_cov) - except LinAlgError as e: - log.warning( - "Degenerate conditions encountered when " - "computing chi-squared: %s" % (e,) - ) - return np.inf + return self._calc_gls_chi2() else: # Residual units are in seconds. Error units are in microseconds. toa_errors = self.get_data_error() diff --git a/src/pint/utils.py b/src/pint/utils.py index e58b90f87..97e7ed213 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -40,6 +40,7 @@ from contextlib import contextmanager from pathlib import Path from warnings import warn +import scipy import uncertainties import astropy.constants as const @@ -2138,7 +2139,7 @@ def normalize_designmatrix(M, params): norm = np.sqrt(np.sum(M**2, axis=0)) bad_params = [params[i] for i in np.where(norm == 0)[0]] - if len(bad_params) > 0: + if len(bad_params) > 0 and params is not None: warn( f"Parameter degeneracy found in designmatrix! The offending parameters are {bad_params}.", DegeneracyWarning, @@ -2147,3 +2148,49 @@ def normalize_designmatrix(M, params): M1 = M / norm return M1, norm + + +def calc_gls_chi2(resids, threshold=0): + resids.update() + + residuals = resids.time_resids.to(u.s).value + + M = resids.model.noise_model_designmatrix(resids.toas) + phi = resids.model.noise_model_basis_weight(resids.toas) + phiinv = 1 / phi + + # For implicit offset subtraction + if "PHOFF" not in resids.model: + M = np.append(M, np.ones((len(resids.toas), 1)), axis=1) + phiinv = np.append(phiinv, [0]) + + M, norm = normalize_designmatrix(M, None) + + phiinv /= norm**2 + Nvec = resids.model.scaled_toa_uncertainty(resids.toas).to(u.s).value ** 2 + cinv = 1 / Nvec + mtcm = np.dot(M.T, cinv[:, None] * M) + mtcm += np.diag(phiinv) + mtcy = np.dot(M.T, cinv * residuals) + + log.trace(f"mtcm: {mtcm}") + xhat = None + if threshold <= 0: + try: + c = scipy.linalg.cho_factor(mtcm) + xhat = scipy.linalg.cho_solve(c, mtcy) + except scipy.linalg.LinAlgError: + xhat = None + if xhat is None: + U, s, Vt = scipy.linalg.svd(mtcm, full_matrices=False) + + bad = np.where(s <= threshold * s[0])[0] + s[bad] = np.inf + + xhat = np.dot(Vt.T, np.dot(U.T, mtcy) / s) + + newres = residuals - np.dot(M, xhat) + # compute linearized chisq + chi2 = np.dot(newres, cinv * newres) + np.dot(xhat, phiinv * xhat) + + return chi2 From 2cd6792e5b584a8689b39ec87c86cda01ca99483 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 17:03:19 -0500 Subject: [PATCH 035/144] normalize_designmatrix --- src/pint/models/timing_model.py | 13 ++-------- src/pint/utils.py | 46 --------------------------------- 2 files changed, 2 insertions(+), 57 deletions(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index d6872bcb4..a91586eb6 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -62,6 +62,7 @@ from pint.toa import TOAs from pint.utils import ( PrefixError, + normalize_designmatrix, split_prefixed_name, open_or_use, colorize, @@ -1892,17 +1893,7 @@ def designmatrix( if not normalize: return M, params, units else: - norm = np.sqrt(np.sum(M**2, axis=0)) - - if np.any(norm == 0): - offending_params = [params[i] for i in np.where(norm == 0)[0]] - warn( - f"Parameter degeneracy found in design matrix! The offending parameters are {offending_params}." - ) - - norm[norm == 0] = 1 - M /= norm - + M, norm = normalize_designmatrix(M, params) return M, norm, params, units def compare( diff --git a/src/pint/utils.py b/src/pint/utils.py index 97e7ed213..eb10b6baf 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -2148,49 +2148,3 @@ def normalize_designmatrix(M, params): M1 = M / norm return M1, norm - - -def calc_gls_chi2(resids, threshold=0): - resids.update() - - residuals = resids.time_resids.to(u.s).value - - M = resids.model.noise_model_designmatrix(resids.toas) - phi = resids.model.noise_model_basis_weight(resids.toas) - phiinv = 1 / phi - - # For implicit offset subtraction - if "PHOFF" not in resids.model: - M = np.append(M, np.ones((len(resids.toas), 1)), axis=1) - phiinv = np.append(phiinv, [0]) - - M, norm = normalize_designmatrix(M, None) - - phiinv /= norm**2 - Nvec = resids.model.scaled_toa_uncertainty(resids.toas).to(u.s).value ** 2 - cinv = 1 / Nvec - mtcm = np.dot(M.T, cinv[:, None] * M) - mtcm += np.diag(phiinv) - mtcy = np.dot(M.T, cinv * residuals) - - log.trace(f"mtcm: {mtcm}") - xhat = None - if threshold <= 0: - try: - c = scipy.linalg.cho_factor(mtcm) - xhat = scipy.linalg.cho_solve(c, mtcy) - except scipy.linalg.LinAlgError: - xhat = None - if xhat is None: - U, s, Vt = scipy.linalg.svd(mtcm, full_matrices=False) - - bad = np.where(s <= threshold * s[0])[0] - s[bad] = np.inf - - xhat = np.dot(Vt.T, np.dot(U.T, mtcy) / s) - - newres = residuals - np.dot(M, xhat) - # compute linearized chisq - chi2 = np.dot(newres, cinv * newres) + np.dot(xhat, phiinv * xhat) - - return chi2 From 9f4387942001ca44f0639fee498f753cea157cb1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 17:08:36 -0500 Subject: [PATCH 036/144] cleanup --- src/pint/models/timing_model.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index a91586eb6..aad292c09 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -1808,9 +1808,7 @@ def d_dm_d_param(self, data, param): ) return result - def designmatrix( - self, toas, acc_delay=None, incfrozen=False, incoffset=True, normalize=False - ): + def designmatrix(self, toas, acc_delay=None, incfrozen=False, incoffset=True): """Return the design matrix. The design matrix is the matrix with columns of ``d_phase_d_param/F0`` @@ -1890,11 +1888,7 @@ def designmatrix( M[:, ii] = q.to_value(the_unit) / F0.value units.append(the_unit / F0.unit) - if not normalize: - return M, params, units - else: - M, norm = normalize_designmatrix(M, params) - return M, norm, params, units + return M, params, units def compare( self, From f4dcdb505f765f87020bf1ee450e0f1848d2da30 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 17:12:24 -0500 Subject: [PATCH 037/144] CHANGELOG --- CHANGELOG-unreleased.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 017a324b2..9decae825 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -12,6 +12,8 @@ the released changes. - Third-order Roemer delay terms to ELL1 model - Made the addition of a TZR TOA (`AbsPhase`) in the `TimingModel` explicit in `Residuals` class. - Updated `CONTRIBUTING.rst` with the latest information. +- Moved design matrix normalization code from `pint.fitter` to the new `pint.utils.normalize_designmatrix()` function. +- Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). ### Added - Third-order Roemer delay terms to ELL1 model - Options to add a TZR TOA (`AbsPhase`) during the creation of a `TimingModel` using `ModelBuilder.__call__`, `get_model`, and `get_model_and_toas` From 7e09c715db298f2f68645ddeb12783b8437c1850 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 14 Aug 2023 18:13:49 -0500 Subject: [PATCH 038/144] fix get_designmatrix --- src/pint/fitter.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 0d7137d18..a9c223b82 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -716,11 +716,9 @@ def update_resids(self): def make_resids(self, model): return Residuals(toas=self.toas, model=model, track_mode=self.track_mode) - def get_designmatrix(self, normalize=False): + def get_designmatrix(self): """Return the model's design matrix for these TOAs.""" - return self.model.designmatrix( - toas=self.toas, incfrozen=False, incoffset=True, normalize=normalize - ) + return self.model.designmatrix(toas=self.toas, incfrozen=False, incoffset=True) def _get_corr_cov_matrix( self, matrix_type, with_phase, pretty_print, prec, usecolor From 6b3f33014a0515d24f3c079a92c648bec7551e2d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 15 Aug 2023 08:42:59 -0500 Subject: [PATCH 039/144] fix fitter --- src/pint/fitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index a9c223b82..7493d3453 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -2153,7 +2153,7 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): # Define the linear system # normalize the design matrix - M, norm, params, units = self.get_designmatrix(normalize=True) + M, norm, params, units = self.get_designmatrix() self.fac = norm # M /= norm From 093deb344f66e064404b548b07aa80b83d42e2d2 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 15 Aug 2023 08:44:03 -0500 Subject: [PATCH 040/144] -- --- src/pint/fitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 7493d3453..46720b350 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -2153,7 +2153,7 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): # Define the linear system # normalize the design matrix - M, norm, params, units = self.get_designmatrix() + M, params, units = self.get_designmatrix() self.fac = norm # M /= norm From 659d3c82da86522e0cca28d91b12b61aaea364aa Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 15 Aug 2023 08:46:00 -0500 Subject: [PATCH 041/144] -- --- src/pint/fitter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 46720b350..38f123c82 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -2154,8 +2154,6 @@ def fit_toas(self, maxiter=1, threshold=0, full_cov=False, debug=False): # Define the linear system # normalize the design matrix M, params, units = self.get_designmatrix() - - self.fac = norm # M /= norm ntmpar = len(fitp) From 7188ccc5e06cfc3fc62fb90373d4e129b6fedfe4 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 15 Aug 2023 10:44:38 -0500 Subject: [PATCH 042/144] Add fitter and deriv test --- tests/test_wavex.py | 79 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 7061a51ac..e8b49deab 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -1,9 +1,11 @@ from io import StringIO import pytest import numpy as np +import logging from astropy import units as u from pint.models import get_model, get_model_and_toas +from pint.models import model_builder as mb from pint.models.timing_model import Component from pint.fitter import Fitter from pint.residuals import Residuals @@ -11,6 +13,7 @@ from pint.simulation import make_fake_toas_uniform import pint.utils from pinttestdata import datadir +from pint.models.wavex import WaveX par1 = """ PSR B1937+21 @@ -73,11 +76,68 @@ def wavex_delay(waves, toas): return total_delay -# def test_derivative(): -# assert None +def test_derivative(): + model = mb.get_model(StringIO(par2)) + model.WXFREQ_0001.value = 0.1 + model.WXSIN_0001.value = 0.01 + model.WXCOS_0001.value = 0.05 + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + log = logging.getLogger("TestWaveXDerivative") + p = "WXSIN_0001" + log.debug("Running derivative for %s", f"d_delay_d_{p}") + ndf = model.d_delay_d_param_num(toas, p) + adf = model.d_delay_d_param(toas, p) + diff = ndf - adf + print(diff) + if np.all(diff.value) != 0.0: + mean_der = (adf + ndf) / 2.0 + relative_diff = np.abs(diff) / np.abs(mean_der) + msg = ( + "Derivative test failed at d_delay_d_%s with max relative difference %lf" + % (p, np.nanmax(relative_diff).value) + ) + tol = 0.7 + log.debug( + ( + "derivative relative diff for %s, %lf" + % (f"d_delay_d_{p}", np.nanmax(relative_diff).value) + ) + ) + assert np.nanmax(relative_diff) < tol, msg + + +def test_wxsin_fit(): + # Check that when a par file with a wavex model is used to generate fake toas the wavex parameters don't change much when fitted for + model = get_model(StringIO(par1)) + model.add_component(WaveX()) + model.WXFREQ_0001.value = 0.1 + model.WXSIN_0001.value = 0.01 + model.WXCOS_0001.value = 0.05 + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + for param in model.free_params: + getattr(model, param).frozen = True + model.WXSIN_0001.value = 0.02 + model.WXSIN_0001.frozen = False + f = Fitter.auto(toas, model) + f.fit_toas() + assert np.isclose(f.model.WXSIN_0001.value, 0.01, atol=1e-3) + -# def test_fitter(): -# assert +def test_wxcos_fit(): + # Check that when a par file with a wavex model is used to generate fake toas the wavex parameters don't change much when fitted for + model = get_model(StringIO(par1)) + model.add_component(WaveX()) + model.WXFREQ_0001.value = 0.1 + model.WXSIN_0001.value = 0.01 + model.WXCOS_0001.value = 0.05 + toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") + for param in model.free_params: + getattr(model, param).frozen = True + model.WXCOS_0001.value = 0.09 + model.WXCOS_0001.frozen = False + f = Fitter.auto(toas, model) + f.fit_toas() + assert np.isclose(f.model.WXCOS_0001.value, 0.05, atol=1e-3) def test_wavex_resids_amp(): @@ -87,8 +147,7 @@ def test_wavex_resids_amp(): wave_model = get_model(StringIO(par2)) rs = Residuals(toas, wave_model) injected_amp = np.sqrt( - wave_model.components["WaveX"].WXSIN_0001.quantity ** 2 - + wave_model.components["WaveX"].WXCOS_0001.quantity ** 2 + wave_model.WXSIN_0001.quantity**2 + wave_model.WXCOS_0001.quantity**2 ) assert np.isclose(max(rs.resids), injected_amp, atol=1e-2) assert np.isclose(min(rs.resids), -injected_amp, atol=1e-2) @@ -115,11 +174,11 @@ def test_add_wavex_to_par(): # Add a wavex component to par file that has none and check against par file with some WaveX model model = get_model(StringIO(par1)) toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") - model.add_component(Component.component_types["WaveX"]()) + model.add_component(WaveX()) index = model.components["WaveX"].get_indices() - model.components["WaveX"].WXFREQ_0001.quantity = 0.1 * (1 / u.d) - model.components["WaveX"].WXSIN_0001.quantity = 1 * u.s - model.components["WaveX"].WXCOS_0001.quantity = 1 * u.s + model.WXFREQ_0001.quantity = 0.1 * (1 / u.d) + model.WXSIN_0001.quantity = 1 * u.s + model.WXCOS_0001.quantity = 1 * u.s wavex_model = get_model(StringIO(par2)) assert np.all( np.array(index) == np.array(wavex_model.components["WaveX"].get_indices()) From 9397bcf67cfe70666b87ca5a7f55d589fb2ca4ba Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 15 Aug 2023 14:02:44 -0500 Subject: [PATCH 043/144] work on utils functions --- src/pint/utils.py | 31 +++++++++++++++++++++++++++++++ tests/test_wavex.py | 1 + 2 files changed, 32 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 84f2040a6..921437b0d 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1265,6 +1265,37 @@ def split_swx(model, time): return index, newindex +def wavex_setup(fitter, freqs=None, n_freqs=None): + """Set-up a WaveX model based on either an array of user-provided frequencies or the wave number + frequency calculation. Sine and Cosine amplitudes are initially set to zero + + + + Parameters + ---------- + fitter: PINT fitter object, None + freqs: iterable of float or astropy.quantity.Quantity + User inputed base frequencies + n_freqs: int, None + Number of wave frequencies to calculate using the equation: freq_n = 2 * pi * n / T_span + Where n is the wave number, and T_span is the total time span of the toas in the fitter object + """ + from pint.models.wavex import WaveX + + if (freqs is None) and (n_freqs is None): + raise ValueError( + "WaveX component base frequencies are not specified" + "Please input either freqs or n_freqs" + ) + + if freqs is not None: + fitter.model.add_component(WaveX()) + if len(freqs) == 1: + fitter.model.WXFREQ_0001.value = freqs + else: + fitter.model.WXFREQ_0001.value = freqs[0] + + def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): """Compute weighted mean of input values diff --git a/tests/test_wavex.py b/tests/test_wavex.py index e8b49deab..247a2e2eb 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -77,6 +77,7 @@ def wavex_delay(waves, toas): def test_derivative(): + # Check that analytical and numerical derivatives are similar model = mb.get_model(StringIO(par2)) model.WXFREQ_0001.value = 0.1 model.WXSIN_0001.value = 0.01 From 1399c99f337c1f0fa6ad91ff54e91979ae9a7f0d Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 15 Aug 2023 15:50:41 -0500 Subject: [PATCH 044/144] Add setup helper function --- src/pint/utils.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 921437b0d..3c30b74de 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1269,7 +1269,7 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): """Set-up a WaveX model based on either an array of user-provided frequencies or the wave number frequency calculation. Sine and Cosine amplitudes are initially set to zero - + User specifies either freqs or n_freqs Parameters ---------- @@ -1288,12 +1288,34 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): "Please input either freqs or n_freqs" ) + if (freqs is not None) and (n_freqs is not None): + raise ValueError( + "Both freqs and n_freqs are specified. Only one or the other should be used" + ) + + if n_freqs == 0: + raise ValueError("Must use a non-zero number of wave freequencies") + if freqs is not None: fitter.model.add_component(WaveX()) if len(freqs) == 1: fitter.model.WXFREQ_0001.value = freqs else: fitter.model.WXFREQ_0001.value = freqs[0] + fitter.model.components["WaveX"].add_wavex_components(freqs[1:]) + + if n_freqs is not None: + fitter.model.add_component(WaveX()) + T_span = fitter.toas.get_mjds().max() - fitter.toas.get_mjds().min() + if n_freqs == 1: + wave_freq = 2.0 * np.pi / T_span + fitter.model.WXFREQ_0001.value = wave_freq + else: + wave_numbers = np.arange(1, n_freqs + 1) + wave_freqs = 2.0 * np.pi * wave_numbers / T_span + fitter.model.WXFREQ_0001.value = wave_freqs[0] + fitter.model.components["WaveX"].add_wavex_components(wave_freqs[1:]) + return fitter.model.components["WaveX"].get_indices() def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): From 66b72eee13c6677a2b36a4f5e9dfa494d1ee2589 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 15 Aug 2023 16:04:28 -0500 Subject: [PATCH 045/144] Fixes to setup utils function --- src/pint/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 3c30b74de..22c5faae6 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1269,7 +1269,8 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): """Set-up a WaveX model based on either an array of user-provided frequencies or the wave number frequency calculation. Sine and Cosine amplitudes are initially set to zero - User specifies either freqs or n_freqs + User specifies either freqs or n_freqs. This function assumes that the timing model does not already + have any WaveX components Parameters ---------- @@ -1284,7 +1285,7 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): if (freqs is None) and (n_freqs is None): raise ValueError( - "WaveX component base frequencies are not specified" + "WaveX component base frequencies are not specified. " "Please input either freqs or n_freqs" ) @@ -1294,7 +1295,7 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): ) if n_freqs == 0: - raise ValueError("Must use a non-zero number of wave freequencies") + raise ValueError("Must use a non-zero number of wave frequencies") if freqs is not None: fitter.model.add_component(WaveX()) From 2c3faaf64d49526216dfc7b7391947a2ed68a98c Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 15 Aug 2023 20:35:19 -0500 Subject: [PATCH 046/144] Fixed astropy table issue --- src/pint/models/wavex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index b89bff556..a77feedba 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -358,7 +358,7 @@ def wavex_delay(self, toas, delays): def d_wavex_delay_d_WXSIN(self, toas, param, delays, acc_delay=None): par = getattr(self, param) freq = getattr(self, f"WXFREQ_{int(par.index):04d}").quantity - base_phase = toas.table["tdbld"].value * u.d - self.WXEPOCH.value * u.d + base_phase = toas.table["tdbld"].data * u.d - self.WXEPOCH.value * u.d arg = 2.0 * np.pi * freq * base_phase deriv = np.sin(arg.value) return deriv * u.s / par.units @@ -366,7 +366,7 @@ def d_wavex_delay_d_WXSIN(self, toas, param, delays, acc_delay=None): def d_wavex_delay_d_WXCOS(self, toas, param, delays, acc_delay=None): par = getattr(self, param) freq = getattr(self, f"WXFREQ_{int(par.index):04d}").quantity - base_phase = toas.table["tdbld"].value * u.d - self.WXEPOCH.value * u.d + base_phase = toas.table["tdbld"].data * u.d - self.WXEPOCH.value * u.d arg = 2.0 * np.pi * freq * base_phase deriv = np.cos(arg.value) return deriv * u.s / par.units From 43c960d8b95352154fcdfe01439e2dde6e1b3a5a Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 16 Aug 2023 10:51:59 -0500 Subject: [PATCH 047/144] Added ValueError to utils --- src/pint/utils.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 22c5faae6..6266a7cb9 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1270,7 +1270,8 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): frequency calculation. Sine and Cosine amplitudes are initially set to zero User specifies either freqs or n_freqs. This function assumes that the timing model does not already - have any WaveX components + have any WaveX components. See add_wavex_component() or add_wavex_components to add WaveX components + to an existing WaveX model. Parameters ---------- @@ -1296,18 +1297,23 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): if n_freqs == 0: raise ValueError("Must use a non-zero number of wave frequencies") - + fitter.model.add_component(WaveX()) + T_span = fitter.toas.get_mjds().max() - fitter.toas.get_mjds().min() + nyqist_freq = 1.0 / (2.0 * T_span) if freqs is not None: - fitter.model.add_component(WaveX()) if len(freqs) == 1: fitter.model.WXFREQ_0001.value = freqs else: + np.array(freqs) + freqs.sort() + if min(np.diff(freqs)) < nyqist_freq.value: + raise ValueError( + "Wave frequency spacing is finer than frequency resolution of data" + ) fitter.model.WXFREQ_0001.value = freqs[0] fitter.model.components["WaveX"].add_wavex_components(freqs[1:]) if n_freqs is not None: - fitter.model.add_component(WaveX()) - T_span = fitter.toas.get_mjds().max() - fitter.toas.get_mjds().min() if n_freqs == 1: wave_freq = 2.0 * np.pi / T_span fitter.model.WXFREQ_0001.value = wave_freq From e3dd81afeec858d3ba09d5969731c1e6bc2683fc Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 16 Aug 2023 12:16:47 -0500 Subject: [PATCH 048/144] Added translate wave to wavex utils --- src/pint/utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 6266a7cb9..e048a4651 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1325,6 +1325,35 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): return fitter.model.components["WaveX"].get_indices() +def translate_wave_to_wavex(model): + from pint.models import WaveX + + def translate_wave_freqs(om, k): + return (om * (k + 1)) / (2.0 * np.pi) + + wave_names = [ + "WAVE%d" % ii for ii in range(1, model.components["Wave"].num_wave_terms + 1) + ] + wave_terms = deepcopy( + [getattr(model.components["Wave"], name) for name in wave_names] + ) + wave_om = deepcopy(model.components["Wave"].WAVE_OM.quantity) + model.remove_component("Wave") + model.add_component(WaveX()) + for k, wave_term in enumerate(wave_terms): + wave_sin_amp, wave_cos_amp = wave_term.quantity + wavex_freq = translate_wave_freqs(wave_om, k) + if k == 1: + model.WXFREQ_0001.value = wavex_freq.value + model.WXSIN_0001.value = wave_sin_amp.value + model.WXCOS_0001.value = wave_cos_amp.value + else: + model.components["WaveX"].add_wavex_component( + wavex_freq, wxsin=wave_sin_amp, wxcos=wave_cos_amp + ) + return model.components["WaveX"].get_indices() + + def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): """Compute weighted mean of input values From 027e6ddaf55959b26a44f10d1a33e7ae357d33bc Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 16 Aug 2023 12:21:46 -0500 Subject: [PATCH 049/144] Fix wave epoch --- src/pint/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index e048a4651..05baf6d6d 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1338,8 +1338,10 @@ def translate_wave_freqs(om, k): [getattr(model.components["Wave"], name) for name in wave_names] ) wave_om = deepcopy(model.components["Wave"].WAVE_OM.quantity) + wave_epoch = deepcopy(model.components["Wave"].WAVEEPOCH.quantity) model.remove_component("Wave") model.add_component(WaveX()) + model.WXEPOCH.value = wave_epoch.value for k, wave_term in enumerate(wave_terms): wave_sin_amp, wave_cos_amp = wave_term.quantity wavex_freq = translate_wave_freqs(wave_om, k) From 1aa70627b1edad98f24f743d652d350ec2a8250b Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Wed, 16 Aug 2023 21:05:55 -0500 Subject: [PATCH 050/144] add deepcopy --- src/pint/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 05baf6d6d..dbdc3ad3e 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1327,6 +1327,7 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): def translate_wave_to_wavex(model): from pint.models import WaveX + from copy import deepcopy def translate_wave_freqs(om, k): return (om * (k + 1)) / (2.0 * np.pi) From 0aef628ab57b032cc8be59cae72eabf2b99477a8 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 17 Aug 2023 09:04:40 -0500 Subject: [PATCH 051/144] Added myself to authors --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1a39ab5d5..ff0e5a443 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,6 +16,7 @@ Contributors Active developers are indicated by (*). Authors of the PINT paper are indicated by (#). +* Gabriella Agazie (*) * Akash Anumarlapudi (*) * Anne Archibald (#*) * Matteo Bachetti (#) From cc34402125b6f0c98baec6356d81382753feaa38 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 17 Aug 2023 09:14:35 -0500 Subject: [PATCH 052/144] changelog entry --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 976dd8b87..fff774a2f 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -11,5 +11,6 @@ the released changes. ### Changed ### Added - Third-order Roemer delay terms to ELL1 model +- Added WaveX model as DelayComponent with wave amplitudes as fitted parameters ### Fixed ### Removed From 490819f83e8ecf68e785bf9c87a952620c4228fc Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 17 Aug 2023 10:07:50 -0500 Subject: [PATCH 053/144] fix test_residuals --- tests/test_residuals.py | 47 +++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/test_residuals.py b/tests/test_residuals.py index d4ed15d0d..16a64c2a2 100644 --- a/tests/test_residuals.py +++ b/tests/test_residuals.py @@ -272,32 +272,33 @@ def test_gls_chi2_reasonable(full_cov): toas.adjust_TOAs(TimeDelta(np.random.randn(len(toas)) * u.us)) f = GLSFitter(toas, model) fit_chi2 = f.fit_toas(full_cov=full_cov) - assert_allclose(fit_chi2, f.resids.calc_chi2(full_cov=full_cov)) + assert_allclose(fit_chi2, f.resids.calc_chi2()) +# @abhisrkckl: I am commenting this out because calc_chisq no longer has the full_cov option. # @pytest.mark.xfail(reason="numerical instability maybe?") -def test_gls_chi2_full_cov(): - model = get_model( - StringIO( - """ - PSRJ J1234+5678 - ELAT 0 - ELONG 0 - DM 10 - F0 1 - PEPOCH 58000 - TNRedAmp -14.227505410948254 - TNRedGam 4.91353 - TNRedC 45 - """ - ) - ) - model.free_params = ["ELAT", "ELONG"] - toas = make_fake_toas_uniform(57000, 59000, 100, model=model, error=1 * u.us) - np.random.seed(0) - toas.adjust_TOAs(TimeDelta(np.random.randn(len(toas)) * u.us)) - r = Residuals(toas, model) - assert_allclose(r.calc_chi2(full_cov=True), r.calc_chi2(full_cov=False)) +# def test_gls_chi2_full_cov(): +# model = get_model( +# StringIO( +# """ +# PSRJ J1234+5678 +# ELAT 0 +# ELONG 0 +# DM 10 +# F0 1 +# PEPOCH 58000 +# TNRedAmp -14.227505410948254 +# TNRedGam 4.91353 +# TNRedC 45 +# """ +# ) +# ) +# model.free_params = ["ELAT", "ELONG"] +# toas = make_fake_toas_uniform(57000, 59000, 100, model=model, error=1 * u.us) +# np.random.seed(0) +# toas.adjust_TOAs(TimeDelta(np.random.randn(len(toas)) * u.us)) +# r = Residuals(toas, model) +# assert_allclose(r.calc_chi2(full_cov=True), r.calc_chi2(full_cov=False)) def test_gls_chi2_behaviour(): From bbbd423c389275ec52eb600964ab3b03ebdf5f44 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 17 Aug 2023 11:19:18 -0500 Subject: [PATCH 054/144] Added docstring fixed freqs --- src/pint/utils.py | 49 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 1f600bc18..437ae326f 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -49,6 +49,7 @@ from astropy.time import Time from loguru import logger as log from scipy.special import fdtrc +from copy import deepcopy import pint import pint.pulsar_ecliptic @@ -1281,6 +1282,11 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): n_freqs: int, None Number of wave frequencies to calculate using the equation: freq_n = 2 * pi * n / T_span Where n is the wave number, and T_span is the total time span of the toas in the fitter object + + Returns + ------- + indices : list + Indices that have been assigned to new WaveX components """ from pint.models.wavex import WaveX @@ -1301,12 +1307,16 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): T_span = fitter.toas.get_mjds().max() - fitter.toas.get_mjds().min() nyqist_freq = 1.0 / (2.0 * T_span) if freqs is not None: + if isinstance(freqs, u.quantity.Quantity): + freqs.to(u.d**-1) + else: + freqs *= u.d**-1 if len(freqs) == 1: - fitter.model.WXFREQ_0001.value = freqs + fitter.model.WXFREQ_0001.quantity = freqs else: np.array(freqs) freqs.sort() - if min(np.diff(freqs)) < nyqist_freq.value: + if min(np.diff(freqs.value)) < nyqist_freq.value: raise ValueError( "Wave frequency spacing is finer than frequency resolution of data" ) @@ -1325,12 +1335,37 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): return fitter.model.components["WaveX"].get_indices() +def _translate_wave_freqs(om, k): + """Use Wave model WAVEOM parameter to calculate a WaveX WXFREQ_ frequency parameter for wave number k + + Parameters + ---------- + om: float or astropy.quantity.Quantity + Base frequency of Wave model solution - parameter WAVEOM + k: int + wave number to use to calculate WaveX WXFREQ_ frequency parameter + + Returns + ------- + WXFREQ_ value or quantity that can be used in WaveX model""" + + return (om * (k + 1)) / (2.0 * np.pi) + + def translate_wave_to_wavex(model): - from pint.models import WaveX - from copy import deepcopy + """Go from a Wave model to a WaveX model + + Paramters + --------- + model: pint.models.timing_model.TimingModel + TimingModel containing a Wave model to be converted to a WaveX model - def translate_wave_freqs(om, k): - return (om * (k + 1)) / (2.0 * np.pi) + Returns + ------- + indices : list + Indices that have been assigned to new WaveX components + Editted timing model with Wave model removed and converted WaveX model included""" + from pint.models import WaveX wave_names = [ "WAVE%d" % ii for ii in range(1, model.components["Wave"].num_wave_terms + 1) @@ -1345,7 +1380,7 @@ def translate_wave_freqs(om, k): model.WXEPOCH.value = wave_epoch.value for k, wave_term in enumerate(wave_terms): wave_sin_amp, wave_cos_amp = wave_term.quantity - wavex_freq = translate_wave_freqs(wave_om, k) + wavex_freq = _translate_wave_freqs(wave_om, k) if k == 1: model.WXFREQ_0001.value = wavex_freq.value model.WXSIN_0001.value = wave_sin_amp.value From f2011c5e4cc3a848f2fc448b6e00e71f02ffb908 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 17 Aug 2023 11:27:31 -0500 Subject: [PATCH 055/144] Removed old comment --- src/pint/models/wavex.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index a77feedba..9856ee5f3 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -354,7 +354,6 @@ def wavex_delay(self, toas, delays): total_delay += wxsin * np.sin(arg.value) + wxcos * np.cos(arg.value) return total_delay - # Placeholder for calculations of derivatives def d_wavex_delay_d_WXSIN(self, toas, param, delays, acc_delay=None): par = getattr(self, param) freq = getattr(self, f"WXFREQ_{int(par.index):04d}").quantity From 503ff32b1055c995f53c18ed48d2a3398a4d79aa Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 17 Aug 2023 11:55:14 -0500 Subject: [PATCH 056/144] Edited docstrings --- src/pint/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 437ae326f..c380a1ac7 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1355,6 +1355,11 @@ def _translate_wave_freqs(om, k): def translate_wave_to_wavex(model): """Go from a Wave model to a WaveX model + WaveX frequencies get calculated based on the Wave model WAVEOM parameter and the number of WAVE parameters. + WaveX_freq_k = [WAVEOM * (k+1)] / [2 * pi] + + WaveX amplitudes are taken from the WAVE pair parameters + Paramters --------- model: pint.models.timing_model.TimingModel From 2e3b45029bb3a92fbce696f5a252d7d4a44b46ca Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 17 Aug 2023 15:38:07 -0500 Subject: [PATCH 057/144] cleanup and doc --- src/pint/models/timing_model.py | 1 - src/pint/utils.py | 20 ++++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index aad292c09..136a42e51 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -62,7 +62,6 @@ from pint.toa import TOAs from pint.utils import ( PrefixError, - normalize_designmatrix, split_prefixed_name, open_or_use, colorize, diff --git a/src/pint/utils.py b/src/pint/utils.py index eb10b6baf..b3c7006c8 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -2134,6 +2134,23 @@ def get_unit(parname): def normalize_designmatrix(M, params): + """Normalize each row of the design matrix. + + This is used while computing the GLS chi2 and the GLS fitting step. The + normalized and unnormalized design matrices Mn and M are related by + M = Mn @ S + where S is a diagonal matrix containing the norms. This normalization is + OK because the GLS operations (fitting step, chi2 computation etc.) involve + the form + M @ (M.T @ N.inv() @ M).inv() @ M.T + and it is easy to see that the above expression doesn't change if we replace + M -> Mn. + + Different parameters can have different units and numerically vastly different + design matrix entries. The normalization step forces the design matrix entries + to have similar numericall values and hence improves the numerical precision of + the matrix operations. + """ from pint.fitter import DegeneracyWarning norm = np.sqrt(np.sum(M**2, axis=0)) @@ -2145,6 +2162,5 @@ def normalize_designmatrix(M, params): DegeneracyWarning, ) norm[norm == 0] = 1 - M1 = M / norm - return M1, norm + return M / norm, norm From 7623d79655287cc9785b8b77442c2b64f3d104f1 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 21 Aug 2023 10:30:36 -0500 Subject: [PATCH 058/144] Change wavex_setup to act on model --- src/pint/utils.py | 51 +++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index c380a1ac7..00bc4795a 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -55,6 +55,7 @@ import pint.pulsar_ecliptic from pint.toa_select import TOASelect + __all__ = [ "PosVel", "numeric_partial", @@ -1266,7 +1267,7 @@ def split_swx(model, time): return index, newindex -def wavex_setup(fitter, freqs=None, n_freqs=None): +def wavex_setup(model, T_span, freqs=None, n_freqs=None): """Set-up a WaveX model based on either an array of user-provided frequencies or the wave number frequency calculation. Sine and Cosine amplitudes are initially set to zero @@ -1276,12 +1277,16 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): Parameters ---------- - fitter: PINT fitter object, None - freqs: iterable of float or astropy.quantity.Quantity + model: pint.models.timing_model.TimingModel + freqs: iterable of float or astropy.quantity.Quantity, None User inputed base frequencies n_freqs: int, None Number of wave frequencies to calculate using the equation: freq_n = 2 * pi * n / T_span Where n is the wave number, and T_span is the total time span of the toas in the fitter object + T_span: float, astropy.quantity.Quantity + Time span used to calculate nyquist frequency when using freqs + Time span used to calculate WaveX frequencies when using n_freqs + Usually to be set as the length of the timing baseline the model is being used for Returns ------- @@ -1303,8 +1308,12 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): if n_freqs == 0: raise ValueError("Must use a non-zero number of wave frequencies") - fitter.model.add_component(WaveX()) - T_span = fitter.toas.get_mjds().max() - fitter.toas.get_mjds().min() + model.add_component(WaveX()) + if isinstance(T_span, u.quantity.Quantity): + T_span.to(u.d) + else: + T_span *= u.d + nyqist_freq = 1.0 / (2.0 * T_span) if freqs is not None: if isinstance(freqs, u.quantity.Quantity): @@ -1312,27 +1321,27 @@ def wavex_setup(fitter, freqs=None, n_freqs=None): else: freqs *= u.d**-1 if len(freqs) == 1: - fitter.model.WXFREQ_0001.quantity = freqs + model.WXFREQ_0001.quantity = freqs else: np.array(freqs) freqs.sort() - if min(np.diff(freqs.value)) < nyqist_freq.value: + if min(np.diff(freqs)) < nyqist_freq: raise ValueError( "Wave frequency spacing is finer than frequency resolution of data" ) - fitter.model.WXFREQ_0001.value = freqs[0] - fitter.model.components["WaveX"].add_wavex_components(freqs[1:]) + model.WXFREQ_0001.quantity = freqs[0] + model.components["WaveX"].add_wavex_components(freqs[1:]) if n_freqs is not None: if n_freqs == 1: wave_freq = 2.0 * np.pi / T_span - fitter.model.WXFREQ_0001.value = wave_freq + model.WXFREQ_0001.quantity = wave_freq else: wave_numbers = np.arange(1, n_freqs + 1) wave_freqs = 2.0 * np.pi * wave_numbers / T_span - fitter.model.WXFREQ_0001.value = wave_freqs[0] - fitter.model.components["WaveX"].add_wavex_components(wave_freqs[1:]) - return fitter.model.components["WaveX"].get_indices() + model.WXFREQ_0001.quantity = wave_freqs[0] + model.components["WaveX"].add_wavex_components(wave_freqs[1:]) + return model.components["WaveX"].get_indices() def _translate_wave_freqs(om, k): @@ -1342,13 +1351,17 @@ def _translate_wave_freqs(om, k): ---------- om: float or astropy.quantity.Quantity Base frequency of Wave model solution - parameter WAVEOM + If float is given default units of 1/d assigned k: int wave number to use to calculate WaveX WXFREQ_ frequency parameter Returns ------- - WXFREQ_ value or quantity that can be used in WaveX model""" - + WXFREQ_ quantity in units 1/d that can be used in WaveX model""" + if isinstance(om, u.quantity.Quantity): + om.to(u.d**-1) + else: + om *= u.d**-1 return (om * (k + 1)) / (2.0 * np.pi) @@ -1370,7 +1383,7 @@ def translate_wave_to_wavex(model): indices : list Indices that have been assigned to new WaveX components Editted timing model with Wave model removed and converted WaveX model included""" - from pint.models import WaveX + from pint.models.wavex import WaveX wave_names = [ "WAVE%d" % ii for ii in range(1, model.components["Wave"].num_wave_terms + 1) @@ -1388,11 +1401,11 @@ def translate_wave_to_wavex(model): wavex_freq = _translate_wave_freqs(wave_om, k) if k == 1: model.WXFREQ_0001.value = wavex_freq.value - model.WXSIN_0001.value = wave_sin_amp.value - model.WXCOS_0001.value = wave_cos_amp.value + model.WXSIN_0001.value = -wave_sin_amp.value + model.WXCOS_0001.value = -wave_cos_amp.value else: model.components["WaveX"].add_wavex_component( - wavex_freq, wxsin=wave_sin_amp, wxcos=wave_cos_amp + wavex_freq, wxsin=-wave_sin_amp, wxcos=-wave_cos_amp ) return model.components["WaveX"].get_indices() From acb27f4a5664bca82b87023e50c6082702ed9f4b Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 10:55:39 -0500 Subject: [PATCH 059/144] publish.py --- src/pint/output/publish.py | 205 +++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/pint/output/publish.py diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py new file mode 100644 index 000000000..0aa3a7940 --- /dev/null +++ b/src/pint/output/publish.py @@ -0,0 +1,205 @@ +from pint.models import TimingModel +from pint.models.parameter import ( + AngleParameter, + MJDParameter, + boolParameter, + floatParameter, + intParameter, + maskParameter, + prefixParameter, + strParameter, +) +from pint.toa import TOAs +from pint.residuals import Residuals +from io import StringIO +import numpy as np +from uncertainties import ufloat + + +def publish(model: TimingModel, toas: TOAs): + psrname = model.PSR.value + nfree = len(model.free_params) + ephem = model.EPHEM.value + clock = model.CLOCK.value + timeeph = model.TIMEEPH.value + units = model.UNITS.value + + mjds = toas.get_mjds() + mjd_start, mjd_end = int(min(mjds.value)), int(max(mjds.value)) + data_span_yr = (mjd_end - mjd_start) / 365.25 + ntoas = len(toas) + + toa_type = "Wideband" if toas.is_wideband() else "Narrowband" + fit_method = ( + "GLS" + if np.any([nc.introduces_correlated_errors for nc in model.NoiseComponent_list]) + else "WLS" + ) + + res = Residuals(toas, model) + rms_res = res.calc_time_resids().to("us").value.std() + chi2 = res.chi2 + chi2_red = res.chi2_reduced + + exclude_params = [ + "START", + "FINISH", + "NTOA", + "CHI2", + "DMDATA", + "PSR", + "EPHEM", + "CLOCK", + "UNITS", + "TIMEEPH", + "T2CMETHOD", + "DILATEFREQ", + "INFO", + "ECL", + ] + + with StringIO("w") as tex: + tex.write("\\documentclass{article}\n") + tex.write("\\begin{document}\n") + + tex.write("\\begin{table}\n") + tex.write("\\caption{Parameters for PSR %s}\n" % psrname) + tex.write("\\begin{tabular}{ll}\n") + tex.write("\\hline\\hline\n") + tex.write("\\multicolumn{2}{c}{Dataset and Fit summary}\\\\ \n") + tex.write("\\hline\n") + tex.write("Pulsar name \\dotfill & %s \\\\ \n" % psrname) + tex.write( + "MJD range \\dotfill & %d---%d \\\\ \n" + % (mjd_start, mjd_end) + ) + tex.write( + "Data span (yr) \\dotfill & %.2f \\\\ \n" % data_span_yr + ) + tex.write("Number of TOAs \\dotfill & %d \\\\ \n" % ntoas) + tex.write("Number of free parameters \\dotfill & %d \\\\ \n" % nfree) + tex.write("TOA paradigm \\dotfill & %s \\\\ \n" % toa_type) + tex.write( + "Fitting method \\dotfill & %s \\\\ \n" % fit_method + ) + tex.write("Solar system ephemeris \\dotfill & %s \\\\ \n" % ephem) + tex.write("Timescale \\dotfill & %s \\\\ \n" % clock) + tex.write("Time unit \\dotfill & %s \\\\ \n" % units) + tex.write("Time ephemeris \\dotfill & %s \\\\ \n" % timeeph) + tex.write("RMS TOA residuals ($\\mu s$) \\dotfill & %.2f \\\\ \n" % rms_res) + tex.write("chi2 \\dotfill & %.2f \\\\ \n" % chi2) + tex.write("Reduced chi2 \\dotfill & %.2f \\\\ \n" % chi2_red) + tex.write("\\hline\n") + + tex.write("\multicolumn{2}{c}{Measured Quantities} \\\\ \n") + for fp in model.free_params: + param = getattr(model, fp) + if isinstance(param, MJDParameter): + uf = ufloat(param.value, param.uncertainty_value) + tex.write( + "%s, %s (%s)\dotfill & %s \\\\ \n" + % (param.name, param.description, str(param.units), f"{uf:.1uS}") + ) + elif isinstance(param, maskParameter): + tex.write( + "%s %s %s, %s (%s)\dotfill & %s \\\\ \n" + % ( + param.prefix, + param.key, + " ".join(param.key_value), + param.description, + str(param.units), + f"{param.as_ufloat():.1uS}", + ) + ) + else: + tex.write( + "%s, %s (%s)\dotfill & %s \\\\ \n" + % ( + param.name, + param.description, + str(param.units), + f"{param.as_ufloat():.1uS}", + ) + ) + tex.write("\\hline\n") + + tex.write("\multicolumn{2}{c}{Set Quantities} \\\\ \n") + tex.write("\\hline\n") + for p in model.params: + param = getattr(model, p) + if param.value is not None and param.frozen and p not in exclude_params: + if isinstance(param, maskParameter): + tex.write( + "%s %s %s, %s (%s)\dotfill & %f \\\\ \n" + % ( + param.prefix, + param.key, + " ".join(param.key_value), + param.description, + str(param.units), + param.value, + ) + ) + elif isinstance( + param, + (floatParameter, AngleParameter, MJDParameter, prefixParameter), + ): + tex.write( + "%s, %s (%s)\dotfill & %f \\\\ \n" + % (param.name, param.description, str(param.units), param.value) + ) + elif isinstance(param, MJDParameter): + tex.write( + "%s, %s (%s)\dotfill & %f \\\\ \n" + % (param.name, param.description, str(param.units), param.value) + ) + elif isinstance(param, strParameter): + tex.write( + "%s, %s \dotfill & %s \\\\ \n" + % (param.name, param.description, param.value) + ) + elif isinstance(param, boolParameter): + tex.write( + "%s, %s (Y/N)\dotfill & %s \\\\ \n" + % (param.name, param.description, "Y" if param.value else "N") + ) + elif isinstance(param, intParameter): + tex.write( + "%s, %s \dotfill & %d \\\\ \n" + % (param.name, param.description, param.value) + ) + + # tex.write("%s, %s (%s)\dotfill & %f \\\\ \n" % (param.name, param.description, str(param.units), param.value)) + + # Epoch of frequency determination (MJD)\dotfill & 53750 \\ + # Epoch of position determination (MJD)\dotfill & 53750 \\ + # Epoch of dispersion measure determination (MJD)\dotfill & 53750 \\ + # NE_SW (cm^-3)\dotfill & 0 \\ + # \hline + # \multicolumn{2}{c}{Derived Quantities} \\ + # \hline + # $\log_{10}$(Characteristic age, yr) \dotfill & 8.92 \\ + # $\log_{10}$(Surface magnetic field strength, G) \dotfill & 9.36 \\ + # $\log_{10}$(Edot, ergs/s) \dotfill & 33.46 \\ + # \hline + # \multicolumn{2}{c}{Assumptions} \\ + # \hline + # Clock correction procedure\dotfill & TT(BIPM2019) \\ + # Solar system ephemeris model\dotfill & DE421 \\ + # Binary model\dotfill & NONE \\ + # TDB units (tempo1 mode)\dotfill & Y \\ + # FB90 time ephemeris (tempo1 mode)\dotfill & Y \\ + # Shapiro delay due to planets\dotfill & N \\ + # Tropospheric delay\dotfill & N \\ + # Dilate frequency\dotfill & N \\ + # Electron density at 1 AU (cm$^{-3}$)\dotfill & 0.00 \\ + # Model version number\dotfill & 2.00 \\ + tex.write("\\hline\n") + tex.write("\\end{tabular}\n") + tex.write("\\end{table}\n") + tex.write("\\end{document}\n") + + output = tex.getvalue() + + return output From 0a3ca915cd6c0f8e32b1cde6464078c2e26fbe6c Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 10:56:04 -0500 Subject: [PATCH 060/144] param desc --- src/pint/models/spindown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/models/spindown.py b/src/pint/models/spindown.py index 78bbba8cf..ff9972968 100644 --- a/src/pint/models/spindown.py +++ b/src/pint/models/spindown.py @@ -108,7 +108,7 @@ def F_terms(self): def F_description(self, n): """Template function for description""" - return "Spin-frequency %d derivative" % n # if n else "Spin-frequency" + return "Spin-frequency derivative %d" % n if n > 0 else "Spin-frequency" def F_unit(self, n): """Template function for unit""" From 24c65a77695abbd4fc699fae09d5dbb03324f2dc Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 10:56:36 -0500 Subject: [PATCH 061/144] as_ufloat --- src/pint/models/parameter.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index 8a14e0106..31c43107c 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -1297,6 +1297,32 @@ def _print_uncertainty(self, unc): angle_arcsec /= 15.0 return angle_arcsec.to_string(decimal=True, precision=20) + def as_ufloat(self, units=None): + """Return the parameter as a :class:`uncertainties.ufloat` + + Will cast to the specified units, or the default + If the uncertainty is not set will be returned as 0 + + Parameters + ---------- + units : astropy.units.core.Unit, optional + Units to cast the value + + Returns + ------- + uncertainties.ufloat + + Notes + ----- + Currently :class:`~uncertainties.ufloat` does not support double precision values, + so some precision may be lost. + """ + if units is None: + units = self.units + value = self.quantity.to_value(units) if self.quantity is not None else 0 + error = self.uncertainty.to_value(units) if self.uncertainty is not None else 0 + return ufloat(value, error) + class prefixParameter: """Families of parameters identified by a prefix like ``DMX_0123``. From ea1021e0d336a92c73f5f3e9bae8fda8b2b35aa4 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 11:12:07 -0500 Subject: [PATCH 062/144] options --- src/pint/output/publish.py | 101 +++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 31 deletions(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 0aa3a7940..36c6372a2 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -1,4 +1,5 @@ from pint.models import TimingModel +from pint.models.noise_model import NoiseComponent from pint.models.parameter import ( AngleParameter, MJDParameter, @@ -16,7 +17,14 @@ from uncertainties import ufloat -def publish(model: TimingModel, toas: TOAs): +def publish( + model: TimingModel, + toas: TOAs, + include_dmx=False, + include_noise=False, + include_jumps=True, + include_zeros=False, +): psrname = model.PSR.value nfree = len(model.free_params) ephem = model.EPHEM.value @@ -58,6 +66,12 @@ def publish(model: TimingModel, toas: TOAs): "ECL", ] + exclude_prefixes = [] + if not include_dmx: + exclude_prefixes.append("DMX") + if not include_jumps: + exclude_prefixes.append("JUMP") + with StringIO("w") as tex: tex.write("\\documentclass{article}\n") tex.write("\\begin{document}\n") @@ -93,42 +107,69 @@ def publish(model: TimingModel, toas: TOAs): tex.write("\multicolumn{2}{c}{Measured Quantities} \\\\ \n") for fp in model.free_params: - param = getattr(model, fp) - if isinstance(param, MJDParameter): - uf = ufloat(param.value, param.uncertainty_value) - tex.write( - "%s, %s (%s)\dotfill & %s \\\\ \n" - % (param.name, param.description, str(param.units), f"{uf:.1uS}") - ) - elif isinstance(param, maskParameter): - tex.write( - "%s %s %s, %s (%s)\dotfill & %s \\\\ \n" - % ( - param.prefix, - param.key, - " ".join(param.key_value), - param.description, - str(param.units), - f"{param.as_ufloat():.1uS}", + if fp not in exclude_params and all( + [not fp.startswith(pre) for pre in exclude_prefixes] + ): + param = getattr(model, fp) + + if isinstance(param._parent, NoiseComponent) and not include_noise: + continue + + if param.value == 0 and not include_zeros: + continue + + if isinstance(param, MJDParameter): + uf = ufloat(param.value, param.uncertainty_value) + tex.write( + "%s, %s (%s)\dotfill & %s \\\\ \n" + % ( + param.name, + param.description, + str(param.units), + f"{uf:.1uS}", + ) ) - ) - else: - tex.write( - "%s, %s (%s)\dotfill & %s \\\\ \n" - % ( - param.name, - param.description, - str(param.units), - f"{param.as_ufloat():.1uS}", + elif isinstance(param, maskParameter): + tex.write( + "%s %s %s, %s (%s)\dotfill & %s \\\\ \n" + % ( + param.prefix, + param.key, + " ".join(param.key_value), + param.description, + str(param.units), + f"{param.as_ufloat():.1uS}", + ) + ) + else: + tex.write( + "%s, %s (%s)\dotfill & %s \\\\ \n" + % ( + param.name, + param.description, + str(param.units), + f"{param.as_ufloat():.1uS}", + ) ) - ) tex.write("\\hline\n") tex.write("\multicolumn{2}{c}{Set Quantities} \\\\ \n") tex.write("\\hline\n") for p in model.params: param = getattr(model, p) - if param.value is not None and param.frozen and p not in exclude_params: + + if isinstance(param._parent, NoiseComponent) and not include_noise: + continue + + if param.value == 0 and not include_zeros: + continue + + if ( + param.value is not None + and param.frozen + and p not in exclude_params + and all([not p.startswith(pre) for pre in exclude_prefixes]) + ): if isinstance(param, maskParameter): tex.write( "%s %s %s, %s (%s)\dotfill & %f \\\\ \n" @@ -170,8 +211,6 @@ def publish(model: TimingModel, toas: TOAs): % (param.name, param.description, param.value) ) - # tex.write("%s, %s (%s)\dotfill & %f \\\\ \n" % (param.name, param.description, str(param.units), param.value)) - # Epoch of frequency determination (MJD)\dotfill & 53750 \\ # Epoch of position determination (MJD)\dotfill & 53750 \\ # Epoch of dispersion measure determination (MJD)\dotfill & 53750 \\ From d580c27324dc6864bc5a674fadc5c326833a4ea5 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 21 Aug 2023 11:37:57 -0500 Subject: [PATCH 063/144] Changed wavex_setup exception to warning --- src/pint/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 00bc4795a..ac409f6b5 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -50,6 +50,7 @@ from loguru import logger as log from scipy.special import fdtrc from copy import deepcopy +import warnings import pint import pint.pulsar_ecliptic @@ -1271,8 +1272,8 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): """Set-up a WaveX model based on either an array of user-provided frequencies or the wave number frequency calculation. Sine and Cosine amplitudes are initially set to zero - User specifies either freqs or n_freqs. This function assumes that the timing model does not already - have any WaveX components. See add_wavex_component() or add_wavex_components to add WaveX components + User specifies T_span and either freqs or n_freqs. This function assumes that the timing model does not already + have any WaveX components. See add_wavex_component() or add_wavex_components() to add WaveX components to an existing WaveX model. Parameters @@ -1326,7 +1327,7 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): np.array(freqs) freqs.sort() if min(np.diff(freqs)) < nyqist_freq: - raise ValueError( + warnings.warn( "Wave frequency spacing is finer than frequency resolution of data" ) model.WXFREQ_0001.quantity = freqs[0] From be4ce42dd9c0408ea5b89b81603b63c391775c14 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 11:38:41 -0500 Subject: [PATCH 064/144] fix descriptions --- src/pint/models/absolute_phase.py | 8 +++++--- src/pint/models/frequency_dependent.py | 2 +- src/pint/models/jump.py | 2 +- src/pint/models/noise_model.py | 16 ++++------------ src/pint/models/pulsar_binary.py | 2 +- src/pint/models/timing_model.py | 5 ++++- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/pint/models/absolute_phase.py b/src/pint/models/absolute_phase.py index 9d319e2eb..940bea474 100644 --- a/src/pint/models/absolute_phase.py +++ b/src/pint/models/absolute_phase.py @@ -30,19 +30,21 @@ def __init__(self): super().__init__() self.add_param( MJDParameter( - name="TZRMJD", description="Epoch of the zero phase.", time_scale="utc" + name="TZRMJD", + description="Epoch of the zero phase TOA.", + time_scale="utc", ) ) self.add_param( strParameter( - name="TZRSITE", description="Observatory of the zero phase measured." + name="TZRSITE", description="Observatory of the zero phase TOA." ) ) self.add_param( floatParameter( name="TZRFRQ", units=u.MHz, - description="The frequency of the zero phase measured.", + description="The frequency of the zero phase TOA.", ) ) self.tz_cache = None diff --git a/src/pint/models/frequency_dependent.py b/src/pint/models/frequency_dependent.py index c78359cd0..7e295a615 100644 --- a/src/pint/models/frequency_dependent.py +++ b/src/pint/models/frequency_dependent.py @@ -36,7 +36,7 @@ def __init__(self): name="FD1", units="second", value=0.0, - description="Coefficient of delay as a polynomial function of log-frequency", + description="Coefficient of frequency-dependent delay", # descriptionTplt=lambda x: ( # "%d term of frequency" " dependent coefficients" % x # ), diff --git a/src/pint/models/jump.py b/src/pint/models/jump.py index 17e231d25..b133040d2 100644 --- a/src/pint/models/jump.py +++ b/src/pint/models/jump.py @@ -93,7 +93,7 @@ def __init__(self): maskParameter( name="JUMP", units="second", - description="Amount to jump the selected TOAs by.", + description="Phase jump for selection.", ) ) self.phase_funcs_component += [self.jump_phase] diff --git a/src/pint/models/noise_model.py b/src/pint/models/noise_model.py index 02a5d291a..b0a887961 100644 --- a/src/pint/models/noise_model.py +++ b/src/pint/models/noise_model.py @@ -58,8 +58,7 @@ def __init__( name="EFAC", units="", aliases=["T2EFAC", "TNEF"], - description="A multiplication factor on" - " the measured TOA uncertainties,", + description="A multiplication factor for the measured TOA uncertainties,", ) ) @@ -68,9 +67,7 @@ def __init__( name="EQUAD", units="us", aliases=["T2EQUAD"], - description="An error term added in " - "quadrature to the scaled (by" - " EFAC) TOA uncertainty.", + description="An error term added in quadrature to the TOA uncertainty.", ) ) @@ -78,10 +75,7 @@ def __init__( maskParameter( name="TNEQ", units=u.LogUnit(physical_unit=u.second), - description="An error term added in " - "quadrature to the scaled (by" - " EFAC) TOA uncertainty in " - " the unit of log10(second).", + description="An log10-scale error term added in quadrature to the TOA uncertainty", ) ) self.covariance_matrix_funcs += [self.sigma_scaled_cov_matrix] @@ -307,9 +301,7 @@ def __init__( name="ECORR", units="us", aliases=["TNECORR"], - description="An error term added that" - " correlated all TOAs in an" - " observing epoch.", + description="An error term that correlates all TOAs in an observing epoch.", ) ) diff --git a/src/pint/models/pulsar_binary.py b/src/pint/models/pulsar_binary.py index 5e32079ad..c5089ab7d 100644 --- a/src/pint/models/pulsar_binary.py +++ b/src/pint/models/pulsar_binary.py @@ -158,7 +158,7 @@ def __init__(self): floatParameter( name="M2", units=u.M_sun, - description="Mass of companion in the unit Sun mass", + description="Companion mass", ) ) self.add_param( diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 1bd05bfa2..dd47c38d4 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -308,7 +308,7 @@ def __init__(self, name="", components=[]): self.add_param_from_top( strParameter( name="BINARY", - description="The Pulsar System/Binary model to use.", + description="Pulsar System/Binary model", value=None, ), "", @@ -2909,6 +2909,9 @@ def add_param(self, param, deriv_func=None, setup=False): param.name = prefix + str(idx) param.index = idx + if hasattr(self, f"{prefix}1"): + param.description = getattr(self, f"{prefix}1").description + # A more general check if param.name in self.params: exist_par = getattr(self, param.name) From 4ab24af33280f62d6a23489f92990f0d50b2ad5d Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 21 Aug 2023 11:51:44 -0500 Subject: [PATCH 065/144] wavex_setup returns new model --- src/pint/utils.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index ac409f6b5..00a6fdf5d 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1383,32 +1383,31 @@ def translate_wave_to_wavex(model): ------- indices : list Indices that have been assigned to new WaveX components - Editted timing model with Wave model removed and converted WaveX model included""" + New timing model with Wave model removed and converted WaveX model included""" from pint.models.wavex import WaveX + new_model = deepcopy(model) wave_names = [ "WAVE%d" % ii for ii in range(1, model.components["Wave"].num_wave_terms + 1) ] - wave_terms = deepcopy( - [getattr(model.components["Wave"], name) for name in wave_names] - ) - wave_om = deepcopy(model.components["Wave"].WAVE_OM.quantity) - wave_epoch = deepcopy(model.components["Wave"].WAVEEPOCH.quantity) - model.remove_component("Wave") - model.add_component(WaveX()) - model.WXEPOCH.value = wave_epoch.value + wave_terms = [getattr(model.components["Wave"], name) for name in wave_names] + wave_om = model.components["Wave"].WAVE_OM.quantity + wave_epoch = model.components["Wave"].WAVEEPOCH.quantity + new_model.remove_component("Wave") + new_model.add_component(WaveX()) + new_model.WXEPOCH.value = wave_epoch.value for k, wave_term in enumerate(wave_terms): wave_sin_amp, wave_cos_amp = wave_term.quantity wavex_freq = _translate_wave_freqs(wave_om, k) if k == 1: - model.WXFREQ_0001.value = wavex_freq.value - model.WXSIN_0001.value = -wave_sin_amp.value - model.WXCOS_0001.value = -wave_cos_amp.value + new_model.WXFREQ_0001.value = wavex_freq.value + new_model.WXSIN_0001.value = -wave_sin_amp.value + new_model.WXCOS_0001.value = -wave_cos_amp.value else: - model.components["WaveX"].add_wavex_component( + new_model.components["WaveX"].add_wavex_component( wavex_freq, wxsin=-wave_sin_amp, wxcos=-wave_cos_amp ) - return model.components["WaveX"].get_indices() + return new_model.components["WaveX"].get_indices(), new_model def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): From e695cbdd431f50ebc7c7491aebee871388dd17ab Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 21 Aug 2023 12:03:32 -0500 Subject: [PATCH 066/144] fixed error --- src/pint/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 00a6fdf5d..81f4aa158 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1407,7 +1407,7 @@ def translate_wave_to_wavex(model): new_model.components["WaveX"].add_wavex_component( wavex_freq, wxsin=-wave_sin_amp, wxcos=-wave_cos_amp ) - return new_model.components["WaveX"].get_indices(), new_model + return new_model def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): From a6280d22e07a57e7223a40855e4a6d29d8d0696d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 13:01:04 -0500 Subject: [PATCH 067/144] exclude components --- src/pint/models/noise_model.py | 2 +- src/pint/models/parameter.py | 12 ++ src/pint/output/publish.py | 307 +++++++++++++++++---------------- 3 files changed, 167 insertions(+), 154 deletions(-) diff --git a/src/pint/models/noise_model.py b/src/pint/models/noise_model.py index b0a887961..455e00230 100644 --- a/src/pint/models/noise_model.py +++ b/src/pint/models/noise_model.py @@ -75,7 +75,7 @@ def __init__( maskParameter( name="TNEQ", units=u.LogUnit(physical_unit=u.second), - description="An log10-scale error term added in quadrature to the TOA uncertainty", + description="A log10-scale error term added in quadrature to the TOA uncertainty", ) ) self.covariance_matrix_funcs += [self.sigma_scaled_cov_matrix] diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index 31c43107c..2b1dd846f 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -1163,6 +1163,18 @@ def as_ufloats(self): error = self.uncertainty.to_value(u.d) if self.uncertainty is not None else 0 return ufloat(value1, 0), ufloat(value2, error) + def as_ufloat(self): + """Return the parameter as a :class:`uncertainties.ufloat` + value. + + If the uncertainty is not set will be returned as 0 + + Returns + ------- + uncertainties.ufloat + """ + return ufloat(self.value, self.uncertainty_value) + class AngleParameter(Parameter): """Parameter in angle units. diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 36c6372a2..bab7c960f 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -1,20 +1,46 @@ -from pint.models import TimingModel +from pint.models import ( + TimingModel, + DispersionDMX, + FD, + Glitch, + PhaseJump, + SolarWindDispersionX, +) +from pint.models.absolute_phase import AbsPhase from pint.models.noise_model import NoiseComponent from pint.models.parameter import ( - AngleParameter, - MJDParameter, + Parameter, boolParameter, - floatParameter, intParameter, maskParameter, - prefixParameter, strParameter, ) from pint.toa import TOAs from pint.residuals import Residuals from io import StringIO import numpy as np -from uncertainties import ufloat + + +def publish_param_value(param: Parameter): + if isinstance(param, boolParameter): + return "Y" if param.value else "N" + elif isinstance(param, strParameter): + return param.value + elif isinstance(param, intParameter): + return str(param.value) + else: + return f"{param.as_ufloat():.1uS}" if not param.frozen else f"{param.value:f}" + + +def publish_param_unit(param: Parameter): + return "" if param.units == "" or param.units is None else f" ({param.units})" + + +def publish_param(param): + if isinstance(param, maskParameter): + return f"{param.prefix} {param.key} {' '.join(param.key_value)}, {param.description}{publish_param_unit(param)}\dotfill & {publish_param_value(param)} \\\\ \n" + else: + return f"{param.name}, {param.description}{publish_param_unit(param)}\dotfill & {publish_param_value(param)} \\\\ \n" def publish( @@ -22,22 +48,17 @@ def publish( toas: TOAs, include_dmx=False, include_noise=False, - include_jumps=True, + include_jumps=False, include_zeros=False, + include_fd=False, + include_glitches=False, + include_swx=False, + include_tzr=False, ): - psrname = model.PSR.value - nfree = len(model.free_params) - ephem = model.EPHEM.value - clock = model.CLOCK.value - timeeph = model.TIMEEPH.value - units = model.UNITS.value - mjds = toas.get_mjds() mjd_start, mjd_end = int(min(mjds.value)), int(max(mjds.value)) data_span_yr = (mjd_end - mjd_start) / 365.25 - ntoas = len(toas) - toa_type = "Wideband" if toas.is_wideband() else "Narrowband" fit_method = ( "GLS" if np.any([nc.introduces_correlated_errors for nc in model.NoiseComponent_list]) @@ -45,9 +66,6 @@ def publish( ) res = Residuals(toas, model) - rms_res = res.calc_time_resids().to("us").value.std() - chi2 = res.chi2 - chi2_red = res.chi2_reduced exclude_params = [ "START", @@ -64,93 +82,141 @@ def publish( "DILATEFREQ", "INFO", "ECL", + "BINARY", ] - exclude_prefixes = [] + exclude_components = [] if not include_dmx: - exclude_prefixes.append("DMX") + exclude_components.append(DispersionDMX) if not include_jumps: - exclude_prefixes.append("JUMP") + exclude_components.append(PhaseJump) + if not include_fd: + exclude_components.append(FD) + if not include_noise: + exclude_components.append(NoiseComponent) + if not include_glitches: + exclude_components.append(Glitch) + if not include_swx: + exclude_components.append(SolarWindDispersionX) + if not include_tzr: + exclude_components.append(AbsPhase) with StringIO("w") as tex: tex.write("\\documentclass{article}\n") tex.write("\\begin{document}\n") tex.write("\\begin{table}\n") - tex.write("\\caption{Parameters for PSR %s}\n" % psrname) + tex.write("\\caption{Parameters for PSR %s}\n" % model.PSR.value) tex.write("\\begin{tabular}{ll}\n") tex.write("\\hline\\hline\n") - tex.write("\\multicolumn{2}{c}{Dataset and Fit summary}\\\\ \n") + tex.write("\\multicolumn{2}{c}{Dataset and model summary}\\\\ \n") + tex.write("\\hline\n") + tex.write( + f"Pulsar name \\dotfill & {model.PSR.value} \\\\ \n" + ) + tex.write( + f"MJD range \\dotfill & {mjd_start}---{mjd_end} \\\\ \n" + ) + tex.write( + f"Data span (yr) \\dotfill & {data_span_yr:.2f} \\\\ \n" + ) + tex.write(f"Number of TOAs \\dotfill & {len(toas)} \\\\ \n") + tex.write( + f"TOA paradigm \\dotfill & {'Wideband' if toas.is_wideband() else 'Narrowband'} \\\\ \n" + ) + tex.write( + f"Solar system ephemeris \\dotfill & {model.EPHEM.value} \\\\ \n" + ) + tex.write( + f"Timescale \\dotfill & {model.CLOCK.value} \\\\ \n" + ) + tex.write( + f"Time unit \\dotfill & {model.UNITS.value} \\\\ \n" + ) + tex.write( + f"Time ephemeris \\dotfill & {model.TIMEEPH.value} \\\\ \n" + ) + + if model.BINARY.value is not None: + tex.write( + f"Binary model \\dotfill & {model.BINARY.value} \\\\ \n" + ) + + if "PhaseJump" in model.components: + tex.write( + f"Number of JUMPs \\dotfill & {model.get_number_of_jumps()} \\\\ \n" + ) + + if "DispersionDMX" in model.components: + tex.write( + f"Number of DMX ranges \\dotfill & {len(model.components['DispersionDMX'].get_indices())} \\\\ \n" + ) + + if "SolarWindDispersionX" in model.components: + tex.write( + f"Number of SWX ranges \\dotfill & {len(model.components['SolarWindDispersionX'].get_indices())} \\\\ \n" + ) + + if "Glitch" in model.components: + tex.write( + f"Number of Glitches \\dotfill & {len(model.components['Glitch'].glitch_indices)} \\\\ \n" + ) + + if "FD" in model.components: + tex.write( + f"Number of FD parameters \\dotfill & {model.num_FD_terms} \\\\ \n" + ) + + if "ScaleToaError" in model.components: + tex.write( + f"Number of EFACs \\dotfill & {len(model.EFACs)} \\\\ \n" + ) + tex.write( + f"Number of EQUADs \\dotfill & {len(model.EQUADs)} \\\\ \n" + ) + + if "EcorrNoise" in model.components: + tex.write( + f"Number of ECORRs \\dotfill & {len(model.ECORRs)} \\\\ \n" + ) + + if "ScaleDmError" in model.components: + tex.write( + f"Number of DMEFACs \\dotfill & {len(model.DMEFACs)} \\\\ \n" + ) + tex.write( + f"Number of DMEQUADs \\dotfill & {len(model.DMEQUADs)} \\\\ \n" + ) + + tex.write("\\hline\n") + + tex.write("\\multicolumn{2}{c}{Fit summary}\\\\ \n") tex.write("\\hline\n") - tex.write("Pulsar name \\dotfill & %s \\\\ \n" % psrname) tex.write( - "MJD range \\dotfill & %d---%d \\\\ \n" - % (mjd_start, mjd_end) + f"Number of free parameters \\dotfill & {len(model.free_params)} \\\\ \n" ) + tex.write(f"Fitting method \\dotfill & {fit_method} \\\\ \n") tex.write( - "Data span (yr) \\dotfill & %.2f \\\\ \n" % data_span_yr + f"RMS TOA residuals ($\\mu s$) \\dotfill & {res.calc_time_resids().to('us').value.std():.2f} \\\\ \n" ) - tex.write("Number of TOAs \\dotfill & %d \\\\ \n" % ntoas) - tex.write("Number of free parameters \\dotfill & %d \\\\ \n" % nfree) - tex.write("TOA paradigm \\dotfill & %s \\\\ \n" % toa_type) + tex.write(f"chi2 \\dotfill & {res.chi2:.2f} \\\\ \n") tex.write( - "Fitting method \\dotfill & %s \\\\ \n" % fit_method + f"Reduced chi2 \\dotfill & {res.chi2_reduced:.2f} \\\\ \n" ) - tex.write("Solar system ephemeris \\dotfill & %s \\\\ \n" % ephem) - tex.write("Timescale \\dotfill & %s \\\\ \n" % clock) - tex.write("Time unit \\dotfill & %s \\\\ \n" % units) - tex.write("Time ephemeris \\dotfill & %s \\\\ \n" % timeeph) - tex.write("RMS TOA residuals ($\\mu s$) \\dotfill & %.2f \\\\ \n" % rms_res) - tex.write("chi2 \\dotfill & %.2f \\\\ \n" % chi2) - tex.write("Reduced chi2 \\dotfill & %.2f \\\\ \n" % chi2_red) tex.write("\\hline\n") tex.write("\multicolumn{2}{c}{Measured Quantities} \\\\ \n") for fp in model.free_params: - if fp not in exclude_params and all( - [not fp.startswith(pre) for pre in exclude_prefixes] + param = getattr(model, fp) + if ( + fp not in exclude_params + and all( + [not isinstance(param._parent, exc) for exc in exclude_components] + ) + and (param.value != 0 or include_zeros) ): - param = getattr(model, fp) - - if isinstance(param._parent, NoiseComponent) and not include_noise: - continue - - if param.value == 0 and not include_zeros: - continue - - if isinstance(param, MJDParameter): - uf = ufloat(param.value, param.uncertainty_value) - tex.write( - "%s, %s (%s)\dotfill & %s \\\\ \n" - % ( - param.name, - param.description, - str(param.units), - f"{uf:.1uS}", - ) - ) - elif isinstance(param, maskParameter): - tex.write( - "%s %s %s, %s (%s)\dotfill & %s \\\\ \n" - % ( - param.prefix, - param.key, - " ".join(param.key_value), - param.description, - str(param.units), - f"{param.as_ufloat():.1uS}", - ) - ) - else: - tex.write( - "%s, %s (%s)\dotfill & %s \\\\ \n" - % ( - param.name, - param.description, - str(param.units), - f"{param.as_ufloat():.1uS}", - ) - ) + tex.write(publish_param(param)) + tex.write("\\hline\n") tex.write("\multicolumn{2}{c}{Set Quantities} \\\\ \n") @@ -158,82 +224,17 @@ def publish( for p in model.params: param = getattr(model, p) - if isinstance(param._parent, NoiseComponent) and not include_noise: - continue - - if param.value == 0 and not include_zeros: - continue - if ( param.value is not None and param.frozen and p not in exclude_params - and all([not p.startswith(pre) for pre in exclude_prefixes]) + and all( + [not isinstance(param._parent, exc) for exc in exclude_components] + ) + and (param.value != 0 or include_zeros) ): - if isinstance(param, maskParameter): - tex.write( - "%s %s %s, %s (%s)\dotfill & %f \\\\ \n" - % ( - param.prefix, - param.key, - " ".join(param.key_value), - param.description, - str(param.units), - param.value, - ) - ) - elif isinstance( - param, - (floatParameter, AngleParameter, MJDParameter, prefixParameter), - ): - tex.write( - "%s, %s (%s)\dotfill & %f \\\\ \n" - % (param.name, param.description, str(param.units), param.value) - ) - elif isinstance(param, MJDParameter): - tex.write( - "%s, %s (%s)\dotfill & %f \\\\ \n" - % (param.name, param.description, str(param.units), param.value) - ) - elif isinstance(param, strParameter): - tex.write( - "%s, %s \dotfill & %s \\\\ \n" - % (param.name, param.description, param.value) - ) - elif isinstance(param, boolParameter): - tex.write( - "%s, %s (Y/N)\dotfill & %s \\\\ \n" - % (param.name, param.description, "Y" if param.value else "N") - ) - elif isinstance(param, intParameter): - tex.write( - "%s, %s \dotfill & %d \\\\ \n" - % (param.name, param.description, param.value) - ) - - # Epoch of frequency determination (MJD)\dotfill & 53750 \\ - # Epoch of position determination (MJD)\dotfill & 53750 \\ - # Epoch of dispersion measure determination (MJD)\dotfill & 53750 \\ - # NE_SW (cm^-3)\dotfill & 0 \\ - # \hline - # \multicolumn{2}{c}{Derived Quantities} \\ - # \hline - # $\log_{10}$(Characteristic age, yr) \dotfill & 8.92 \\ - # $\log_{10}$(Surface magnetic field strength, G) \dotfill & 9.36 \\ - # $\log_{10}$(Edot, ergs/s) \dotfill & 33.46 \\ - # \hline - # \multicolumn{2}{c}{Assumptions} \\ - # \hline - # Clock correction procedure\dotfill & TT(BIPM2019) \\ - # Solar system ephemeris model\dotfill & DE421 \\ - # Binary model\dotfill & NONE \\ - # TDB units (tempo1 mode)\dotfill & Y \\ - # FB90 time ephemeris (tempo1 mode)\dotfill & Y \\ - # Shapiro delay due to planets\dotfill & N \\ - # Tropospheric delay\dotfill & N \\ - # Dilate frequency\dotfill & N \\ - # Electron density at 1 AU (cm$^{-3}$)\dotfill & 0.00 \\ - # Model version number\dotfill & 2.00 \\ + tex.write(publish_param(param)) + tex.write("\\hline\n") tex.write("\\end{tabular}\n") tex.write("\\end{table}\n") From 8af98a42fdb7d7b8af371bb8910dce9881af991b Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 14:12:22 -0500 Subject: [PATCH 068/144] fix param namess --- src/pint/models/binary_ell1.py | 4 ++-- src/pint/models/pulsar_binary.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pint/models/binary_ell1.py b/src/pint/models/binary_ell1.py index c0051a446..5d8d6697c 100644 --- a/src/pint/models/binary_ell1.py +++ b/src/pint/models/binary_ell1.py @@ -117,7 +117,7 @@ def __init__(self): floatParameter( name="EPS1", units="", - description="First Laplace-Lagrange parameter, ECC x sin(OM) for ELL1 model", + description="First Laplace-Lagrange parameter", long_double=True, ) ) @@ -126,7 +126,7 @@ def __init__(self): floatParameter( name="EPS2", units="", - description="Second Laplace-Lagrange parameter, ECC x cos(OM) for ELL1 model", + description="Second Laplace-Lagrange parameter", long_double=True, ) ) diff --git a/src/pint/models/pulsar_binary.py b/src/pint/models/pulsar_binary.py index c5089ab7d..98278784e 100644 --- a/src/pint/models/pulsar_binary.py +++ b/src/pint/models/pulsar_binary.py @@ -102,7 +102,9 @@ def __init__(self): ) self.add_param( floatParameter( - name="A1", units=ls, description="Projected semi-major axis, a*sin(i)" + name="A1", + units=ls, + description="Projected semi-major axis of pulsar orbit", ) ) # NOTE: the DOT here takes the value and times 1e-12, tempo/tempo2 can From 52bdcae5950eb04f4e186b0d5a40066db2dc423d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 14:12:41 -0500 Subject: [PATCH 069/144] wideband --- src/pint/output/publish.py | 50 ++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index bab7c960f..45ccbb785 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -5,18 +5,20 @@ Glitch, PhaseJump, SolarWindDispersionX, + AbsPhase, ) -from pint.models.absolute_phase import AbsPhase +from pint.models.dispersion_model import DispersionJump from pint.models.noise_model import NoiseComponent from pint.models.parameter import ( Parameter, boolParameter, + funcParameter, intParameter, maskParameter, strParameter, ) from pint.toa import TOAs -from pint.residuals import Residuals +from pint.residuals import Residuals, WidebandTOAResiduals from io import StringIO import numpy as np @@ -65,7 +67,13 @@ def publish( else "WLS" ) - res = Residuals(toas, model) + if toas.is_wideband(): + res = WidebandTOAResiduals(toas, model) + toares = res.toa + dmres = res.dm + else: + res = Residuals(toas, model) + toares = res exclude_params = [ "START", @@ -89,7 +97,7 @@ def publish( if not include_dmx: exclude_components.append(DispersionDMX) if not include_jumps: - exclude_components.append(PhaseJump) + exclude_components.extend([PhaseJump, DispersionJump]) if not include_fd: exclude_components.append(FD) if not include_noise: @@ -147,6 +155,11 @@ def publish( f"Number of JUMPs \\dotfill & {model.get_number_of_jumps()} \\\\ \n" ) + if "DispersionJump" in model.components: + tex.write( + f"Number of DMJUMPs \\dotfill & {len(model.dm_jumps)} \\\\ \n" + ) + if "DispersionDMX" in model.components: tex.write( f"Number of DMX ranges \\dotfill & {len(model.components['DispersionDMX'].get_indices())} \\\\ \n" @@ -197,15 +210,25 @@ def publish( ) tex.write(f"Fitting method \\dotfill & {fit_method} \\\\ \n") tex.write( - f"RMS TOA residuals ($\\mu s$) \\dotfill & {res.calc_time_resids().to('us').value.std():.2f} \\\\ \n" + f"RMS TOA residuals ($\\mu s$) \\dotfill & {toares.calc_time_resids().to('us').value.std():.2e} \\\\ \n" ) - tex.write(f"chi2 \\dotfill & {res.chi2:.2f} \\\\ \n") + if toas.is_wideband(): + tex.write( + f"RMS DM residuals (pc / cm3) \\dotfill & {dmres.calc_resids().to('pc/cm^3').value.std():.2e} \\\\ \n" + ) tex.write( - f"Reduced chi2 \\dotfill & {res.chi2_reduced:.2f} \\\\ \n" + f"$\\chi^2$ \\dotfill & {res.chi2:.2f} \\\\ \n" ) + if toas.is_wideband(): + tex.write(f"Degrees of freedom \\dotfill & {res.dof} \\\\ \n") + else: + tex.write( + f"Reduced $\\chi^2$ \\dotfill & {res.chi2_reduced:.2f} \\\\ \n" + ) tex.write("\\hline\n") tex.write("\multicolumn{2}{c}{Measured Quantities} \\\\ \n") + tex.write("\\hline\n") for fp in model.free_params: param = getattr(model, fp) if ( @@ -232,14 +255,27 @@ def publish( [not isinstance(param._parent, exc) for exc in exclude_components] ) and (param.value != 0 or include_zeros) + and not isinstance(param, funcParameter) ): tex.write(publish_param(param)) tex.write("\\hline\n") + + tex.write("\multicolumn{2}{c}{Derived Quantities} \\\\ \n") + tex.write("\\hline\n") + for p in model.params: + param = getattr(model, p) + + if param.value is not None and isinstance(param, funcParameter): + tex.write(publish_param(param)) + tex.write("\\hline\n") + tex.write("\\end{tabular}\n") tex.write("\\end{table}\n") tex.write("\\end{document}\n") output = tex.getvalue() + output = output.replace("_", "\\_") + return output From f1286be91afc5736e84123b5000789318ab93873 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 21 Aug 2023 14:31:30 -0500 Subject: [PATCH 070/144] More utils functions --- src/pint/utils.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 81f4aa158..bc50de74f 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1410,6 +1410,98 @@ def translate_wave_to_wavex(model): return new_model +def get_wavex_freqs(model, index=None): + """Return the WaveX frequencies for a timing model. + + If index is specified, returns the frequencies corresponding to the user-provided indices. + If index isn't specified, returns all WaveX frequencies in timing model + + Parameters + ---------- + model: pint.models.timing_model.TimingModel + Timing model from which to return WaveX frequencies + index: : float, int, list, np.ndarray, None + Number or list/array of numbers corresponding to WaveX frequencies to return + + Returns + ------- + List of WXFREQ_ parameters""" + if index == None: + freqs = model.components["WaveX"].get_prefix_mapping_component("WXFREQ_") + if len(freqs) == 1: + values = getattr(model.components["WaveX"], freqs.values()) + else: + values = [ + getattr(model.components["WaveX"], param) for param in freqs.values() + ] + elif isinstance(index, (int, float, np.int64)): + idx_rf = f"{int(index):04d}" + values = getattr(model.components["WaveX"], "WXFREQ_" + idx_rf) + elif isinstance(index, (list, set, np.ndarray)): + idx_rf = [f"{int(idx):04d}" for idx in index] + values = [getattr(model.components["WaveX"], "WXFREQ_" + ind) for ind in idx_rf] + else: + raise TypeError( + f"index most be a float, int, set, list, array, or None - not {type(index)}" + ) + return values + + +def get_wavex_amps(model, index=None): + """Return the WaveX amplitudes for a timing model. + + If index is specified, returns the sine/cosine amplitudes corresponding to the user-provided indices. + If index isn't specified, returns all WaveX sine/cosine amplitudes in timing model + + Parameters + ---------- + model: pint.models.timing_model.TimingModel + Timing model from which to return WaveX frequencies + index: : float, int, list, np.ndarray, None + Number or list/array of numbers corresponding to WaveX amplitudes to return + + Returns + ------- + List of WXSIN_ and WXCOS_ parameters""" + if index == None: + indices = ( + model.components["WaveX"].get_prefix_mapping_component("WXSIN_").keys() + ) + if len(indices) == 1: + values = ( + getattr(model.components["WaveX"], "WXSIN_" + f"{int(indices):04d}"), + getattr(model.components["WaveX"], "WXCOS_" + f"{int(indices):04d}"), + ) + else: + values = [ + ( + getattr(model.components["WaveX"], "WXSIN_" + f"{int(idx):04d}"), + getattr(model.components["WaveX"], "WXCOS_" + f"{int(idx):04d}"), + ) + for idx in indices + ] + elif isinstance(index, (int, float, np.int64)): + idx_rf = f"{int(index):04d}" + values = ( + getattr(model.components["WaveX"], "WXSIN_" + idx_rf), + getattr(model.components["WaveX"], "WXCOS_" + idx_rf), + ) + elif isinstance(index, (list, set, np.ndarray)): + idx_rf = [f"{int(idx):04d}" for idx in index] + values = [ + ( + getattr(model.components["WaveX"], "WXSIN_" + ind), + getattr(model.components["WaveX"], "WXCOS_" + ind), + ) + for ind in idx_rf + ] + else: + raise TypeError( + f"index most be a float, int, set, list, array, or None - not {type(index)}" + ) + return values + + def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): """Compute weighted mean of input values From a23008a0e963a1c4771a0e87629f8ae632ab4ef2 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 14:48:58 -0500 Subject: [PATCH 071/144] script --- setup.cfg | 1 + src/pint/scripts/pintempo.py | 2 +- src/pint/scripts/pintpublish.py | 79 +++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/pint/scripts/pintpublish.py diff --git a/setup.cfg b/setup.cfg index 80a0c6408..52fa9ec59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ console_scripts = convert_parfile = pint.scripts.convert_parfile:main compare_parfiles = pint.scripts.compare_parfiles:main tcb2tdb = pint.scripts.tcb2tdb:main + pintpublish = pint.scripts.pintpublish:main # See the docstring in versioneer.py for instructions. Note that you must diff --git a/src/pint/scripts/pintempo.py b/src/pint/scripts/pintempo.py index 7d21c6323..d99c772ba 100755 --- a/src/pint/scripts/pintempo.py +++ b/src/pint/scripts/pintempo.py @@ -27,7 +27,7 @@ def main(argv=None): parser = argparse.ArgumentParser( - description="Command line interfact to PINT", + description="Command line interface to PINT", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("parfile", help="par file to read model from") diff --git a/src/pint/scripts/pintpublish.py b/src/pint/scripts/pintpublish.py new file mode 100644 index 000000000..e4952ce94 --- /dev/null +++ b/src/pint/scripts/pintpublish.py @@ -0,0 +1,79 @@ +from pint.models import get_model_and_toas +from pint.output.publish import publish +import argparse + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Publication output for PINT", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument("parfile", help="par file to read model from") + parser.add_argument("timfile", help="TOA file name") + parser.add_argument("--outfile", help="Output file", default=None) + parser.add_argument( + "--include_dmx", + help="Include DMX parameters", + action="store_true", + default=False, + ) + parser.add_argument( + "--include_noise", + help="Include noise parameters", + action="store_true", + default=False, + ) + parser.add_argument( + "--include_jumps", help="Include jumps", action="store_true", default=False + ) + parser.add_argument( + "--include_zeros", + help="Include parameters equal to 0", + action="store_true", + default=False, + ) + parser.add_argument( + "--include_fd", help="Include FD parameters", action="store_true", default=False + ) + parser.add_argument( + "--include_glitches", + help="Include glitches", + action="store_true", + default=False, + ) + parser.add_argument( + "--include_swx", + help="Include SWX parameters", + action="store_true", + default=False, + ) + parser.add_argument( + "--include_tzr", + help="Include TZR parameters", + action="store_true", + default=False, + ) + + args = parser.parse_args(argv) + + model, toas = get_model_and_toas(args.parfile, args.timfile) + + output = publish( + model, + toas, + include_dmx=args.include_dmx, + include_noise=args.include_noise, + include_jumps=args.include_jumps, + include_zeros=args.include_zeros, + include_fd=args.include_fd, + include_glitches=args.include_glitches, + include_swx=args.include_swx, + include_tzr=args.include_tzr, + ) + + if args.outfile is None: + print(output) + else: + with open(args.outfile, "w") as f: + f.write(output) From 5e2caddb3d134e247a821f2e53bf0ffd453ac851 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 14:49:16 -0500 Subject: [PATCH 072/144] wave --- src/pint/output/publish.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 45ccbb785..c2eab900c 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -6,6 +6,7 @@ PhaseJump, SolarWindDispersionX, AbsPhase, + Wave, ) from pint.models.dispersion_model import DispersionJump from pint.models.noise_model import NoiseComponent @@ -15,6 +16,7 @@ funcParameter, intParameter, maskParameter, + pairParameter, strParameter, ) from pint.toa import TOAs @@ -93,7 +95,7 @@ def publish( "BINARY", ] - exclude_components = [] + exclude_components = [Wave] if not include_dmx: exclude_components.append(DispersionDMX) if not include_jumps: @@ -200,6 +202,10 @@ def publish( tex.write( f"Number of DMEQUADs \\dotfill & {len(model.DMEQUADs)} \\\\ \n" ) + if "Wave" in model.components: + tex.write( + f"Number of WAVE components \\dotfill & {model.num_wave_terms} \\\\ \n" + ) tex.write("\\hline\n") @@ -232,10 +238,8 @@ def publish( for fp in model.free_params: param = getattr(model, fp) if ( - fp not in exclude_params - and all( - [not isinstance(param._parent, exc) for exc in exclude_components] - ) + all([not isinstance(param._parent, exc) for exc in exclude_components]) + and fp not in exclude_params and (param.value != 0 or include_zeros) ): tex.write(publish_param(param)) @@ -248,12 +252,10 @@ def publish( param = getattr(model, p) if ( - param.value is not None + all([not isinstance(param._parent, exc) for exc in exclude_components]) + and param.value is not None and param.frozen and p not in exclude_params - and all( - [not isinstance(param._parent, exc) for exc in exclude_components] - ) and (param.value != 0 or include_zeros) and not isinstance(param, funcParameter) ): @@ -266,7 +268,7 @@ def publish( for p in model.params: param = getattr(model, p) - if param.value is not None and isinstance(param, funcParameter): + if isinstance(param, funcParameter) and param.value is not None: tex.write(publish_param(param)) tex.write("\\hline\n") From 49466e3e717d57aa277772b6eff1f753482c9e0c Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 14:51:10 -0500 Subject: [PATCH 073/144] log --- src/pint/output/publish.py | 1 - src/pint/scripts/pintpublish.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index c2eab900c..cf5fa7c1a 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -16,7 +16,6 @@ funcParameter, intParameter, maskParameter, - pairParameter, strParameter, ) from pint.toa import TOAs diff --git a/src/pint/scripts/pintpublish.py b/src/pint/scripts/pintpublish.py index e4952ce94..150ff2b9a 100644 --- a/src/pint/scripts/pintpublish.py +++ b/src/pint/scripts/pintpublish.py @@ -1,9 +1,12 @@ from pint.models import get_model_and_toas from pint.output.publish import publish +from pint.logging import setup as setup_log import argparse def main(argv=None): + setup_log(level="WARNING") + parser = argparse.ArgumentParser( description="Publication output for PINT", formatter_class=argparse.ArgumentDefaultsHelpFormatter, From 8d06f356d5f2c9a597e77d36e1368a749dcb02a7 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 15:01:31 -0500 Subject: [PATCH 074/144] docs --- src/pint/output/publish.py | 34 +++++++++++++++++++++++++++++++++ src/pint/scripts/pintpublish.py | 1 + 2 files changed, 35 insertions(+) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index cf5fa7c1a..59011840b 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -1,3 +1,4 @@ +"""Generate LaTeX summary of a timing model and TOAs.""" from pint.models import ( TimingModel, DispersionDMX, @@ -25,6 +26,7 @@ def publish_param_value(param: Parameter): + """Return LaTeX string for a parameter value""" if isinstance(param, boolParameter): return "Y" if param.value else "N" elif isinstance(param, strParameter): @@ -36,10 +38,12 @@ def publish_param_value(param: Parameter): def publish_param_unit(param: Parameter): + """Return LaTeX string for a parameter unit""" return "" if param.units == "" or param.units is None else f" ({param.units})" def publish_param(param): + """Return LaTeX line for a parameter""" if isinstance(param, maskParameter): return f"{param.prefix} {param.key} {' '.join(param.key_value)}, {param.description}{publish_param_unit(param)}\dotfill & {publish_param_value(param)} \\\\ \n" else: @@ -58,6 +62,36 @@ def publish( include_swx=False, include_tzr=False, ): + """Generate LaTeX summary of a given timing model and TOAs. + + Parameters + ---------- + model: pint.model.timing_model.TimingModel + Input timing model + toas: TOAs + Input TOAs + include_dmx: bool + Whether to include DMX paremeters (default is False) + include_noise: bool + Whether to include noise paremeters (default is False) + include_jumps: bool + Whether to include jump paremeters (JUMPs, DMJUMPs) (default is False) + include_zeros: bool + Whether to include paremeters which are zero (default is False) + include_fd: bool + Whether to include FD paremeters (default is False) + include_glitches: bool + Whether to include glitch paremeters (default is False) + include_swx: bool + Whether to include SWX paremeters (default is False) + include_tzr: bool + Whether to include TZR paremeters (default is False) + + Returns + ------- + latex_summary: str + The LaTeX summary + """ mjds = toas.get_mjds() mjd_start, mjd_end = int(min(mjds.value)), int(max(mjds.value)) data_span_yr = (mjd_end - mjd_start) / 365.25 diff --git a/src/pint/scripts/pintpublish.py b/src/pint/scripts/pintpublish.py index 150ff2b9a..69b551dc0 100644 --- a/src/pint/scripts/pintpublish.py +++ b/src/pint/scripts/pintpublish.py @@ -1,3 +1,4 @@ +"""Generate LaTeX summary of a timing model and TOAs.""" from pint.models import get_model_and_toas from pint.output.publish import publish from pint.logging import setup as setup_log From 22de9e23aa088ef2838af8a581b6e06958b30a5d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 17:16:50 -0500 Subject: [PATCH 075/144] test --- src/pint/output/publish.py | 2 +- tests/test_publish.py | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/test_publish.py diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 59011840b..11cc87689 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -262,7 +262,7 @@ def publish( tex.write(f"Degrees of freedom \\dotfill & {res.dof} \\\\ \n") else: tex.write( - f"Reduced $\\chi^2$ \\dotfill & {res.chi2_reduced:.2f} \\\\ \n" + f"Reduced $\\chi^2$ \\dotfill & {res.reduced_chi2:.2f} \\\\ \n" ) tex.write("\\hline\n") diff --git a/tests/test_publish.py b/tests/test_publish.py new file mode 100644 index 000000000..4e1b23cca --- /dev/null +++ b/tests/test_publish.py @@ -0,0 +1,72 @@ +from pinttestdata import datadir +import pytest +from pint.models import get_model_and_toas +from pint.output.publish import publish +from pint.scripts import pintpublish +import os + +data_NGC6440E = get_model_and_toas(datadir / "NGC6440E.par", datadir / "NGC6440E.tim") + + +def test_NGC6440E(): + m, t = data_NGC6440E + output = publish(m, t) + assert "NGC6440E" in output + assert "DE421" in output + + +data_J0613m0200_NANOGrav_9yv1 = get_model_and_toas( + datadir / "J0613-0200_NANOGrav_9yv1.gls.par", + datadir / "J0613-0200_NANOGrav_9yv1.tim", +) + + +@pytest.mark.parametrize("full", [True, False]) +def test_J0613m0200_NANOGrav_9yv1(full): + m, t = data_J0613m0200_NANOGrav_9yv1 + output = publish( + m, t, include_dmx=full, include_fd=full, include_noise=full, include_jumps=full + ) + + assert "J0613-0200" in output + assert "ELL1" in output + assert "Narrowband" in output + assert "freedom" not in output + assert "Reduced" in output + assert not full or "DMX" in output + assert not full or "JUMP" in output + assert not full or "FD1" in output + assert not full or "RNAMP" in output + + +data_J1614m2230_NANOGrav_12yv3_wb = get_model_and_toas( + datadir / "J1614-2230_NANOGrav_12yv3.wb.gls.par", + datadir / "J1614-2230_NANOGrav_12yv3.wb.tim", +) + + +@pytest.mark.parametrize("full", [True, False]) +def test_J1614m2230_NANOGrav_12yv3_wb(full): + m, t = data_J1614m2230_NANOGrav_12yv3_wb + output = publish( + m, t, include_dmx=full, include_fd=full, include_noise=full, include_jumps=full + ) + + assert "DE436" in output + assert "ELL1" in output + assert "Wideband" in output + assert "freedom" in output + assert not full or "DMX" in output + assert not full or "DMJUMP" in output + assert not full or "JUMP" in output + assert "TT(BIPM2017)" in output + + +@pytest.mark.parametrize("file", [True, False]) +def test_script(file): + par, tim = str(datadir / "NGC6440E.par"), str(datadir / "NGC6440E.tim") + outfile = "--outfile=pintpublish_test.tex" if file else "" + args = f"{par} {tim} {outfile}" + pintpublish.main(args.split()) + + assert not file or os.path.isfile("pintpublish_test.tex") From 9f1e4c1274f9e333a0008ddb4a28a0ce0a349ef1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Mon, 21 Aug 2023 17:23:22 -0500 Subject: [PATCH 076/144] changelog --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 2a67299aa..ad4c097d4 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -19,6 +19,7 @@ the released changes. - `pint.print_info()` function for bug reporting - Added an autocorrelation function to check for chain convergence in `event_optimize` - Minor doc updates to explain default NHARMS and missing derivative functions +- `pint.output.publish` module and `pintpublish` script for generating publication (LaTeX) output. ### Fixed - Deleting JUMP1 from flag tables will not prevent fitting - Simulating TOAs from tim file when PLANET_SHAPIRO is true now works From 466957304fc184ad7027683f809bec7aa0dea80c Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 08:10:16 -0500 Subject: [PATCH 077/144] fix test --- tests/test_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_publish.py b/tests/test_publish.py index 4e1b23cca..f8663ff3d 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -11,7 +11,7 @@ def test_NGC6440E(): m, t = data_NGC6440E output = publish(m, t) - assert "NGC6440E" in output + assert "1748-2021E" in output assert "DE421" in output From 7876c30347af1e3d5f960ab4c7ab96c612f214e9 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 12:58:28 -0500 Subject: [PATCH 078/144] latex formatting --- src/pint/output/publish.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 11cc87689..495928913 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -34,12 +34,14 @@ def publish_param_value(param: Parameter): elif isinstance(param, intParameter): return str(param.value) else: - return f"{param.as_ufloat():.1uS}" if not param.frozen else f"{param.value:f}" + return ( + f"${param.as_ufloat():.1uSL}$" if not param.frozen else f"{param.value:f}" + ) def publish_param_unit(param: Parameter): """Return LaTeX string for a parameter unit""" - return "" if param.units == "" or param.units is None else f" ({param.units})" + return "" if param.units == "" or param.units is None else f" ({param.units:latex})" def publish_param(param): @@ -155,7 +157,7 @@ def publish( tex.write("\\multicolumn{2}{c}{Dataset and model summary}\\\\ \n") tex.write("\\hline\n") tex.write( - f"Pulsar name \\dotfill & {model.PSR.value} \\\\ \n" + f"Pulsar name \\dotfill & {model.PSR.value.replace('-','$-$')} \\\\ \n" ) tex.write( f"MJD range \\dotfill & {mjd_start}---{mjd_end} \\\\ \n" @@ -249,11 +251,11 @@ def publish( ) tex.write(f"Fitting method \\dotfill & {fit_method} \\\\ \n") tex.write( - f"RMS TOA residuals ($\\mu s$) \\dotfill & {toares.calc_time_resids().to('us').value.std():.2e} \\\\ \n" + f"RMS TOA residuals ($\\mu s$) \\dotfill & {toares.calc_time_resids().to('us').value.std():.2f} \\\\ \n" ) if toas.is_wideband(): tex.write( - f"RMS DM residuals (pc / cm3) \\dotfill & {dmres.calc_resids().to('pc/cm^3').value.std():.2e} \\\\ \n" + f"RMS DM residuals (pc / cm3) \\dotfill & {dmres.calc_resids().to('pc/cm^3').value.std():.2f} \\\\ \n" ) tex.write( f"$\\chi^2$ \\dotfill & {res.chi2:.2f} \\\\ \n" @@ -296,14 +298,15 @@ def publish( tex.write("\\hline\n") - tex.write("\multicolumn{2}{c}{Derived Quantities} \\\\ \n") - tex.write("\\hline\n") - for p in model.params: - param = getattr(model, p) - - if isinstance(param, funcParameter) and param.value is not None: + derived_params = [ + p for p in model.params if isinstance(getattr(model, p), funcParameter) + ] + if len(derived_params) > 0: + tex.write("\multicolumn{2}{c}{Derived Quantities} \\\\ \n") + tex.write("\\hline\n") + for param in derived_params: tex.write(publish_param(param)) - tex.write("\\hline\n") + tex.write("\\hline\n") tex.write("\\end{tabular}\n") tex.write("\\end{table}\n") From 1296d08b9e3fdb481d8c5d0ccafccb266059b551 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 13:10:59 -0500 Subject: [PATCH 079/144] disable fraction --- src/pint/output/publish.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 495928913..4e99d1006 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -41,7 +41,11 @@ def publish_param_value(param: Parameter): def publish_param_unit(param: Parameter): """Return LaTeX string for a parameter unit""" - return "" if param.units == "" or param.units is None else f" ({param.units:latex})" + return ( + "" + if param.units == "" or param.units is None + else f" ({param.units.to_string(format='latex', fraction=False)})" + ) def publish_param(param): From 1ca2246a3fb7c1795b1260337ecea72770760150 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 13:11:17 -0500 Subject: [PATCH 080/144] desc --- src/pint/models/binary_ell1.py | 4 ++-- src/pint/models/frequency_dependent.py | 2 +- src/pint/models/noise_model.py | 2 +- src/pint/models/pulsar_binary.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pint/models/binary_ell1.py b/src/pint/models/binary_ell1.py index ef0331e2a..8e3c93b4e 100644 --- a/src/pint/models/binary_ell1.py +++ b/src/pint/models/binary_ell1.py @@ -117,7 +117,7 @@ def __init__(self): floatParameter( name="EPS1", units="", - description="First Laplace-Lagrange parameter", + description="First Laplace-Lagrange parameter, ECC*sin(OM)", long_double=True, ) ) @@ -126,7 +126,7 @@ def __init__(self): floatParameter( name="EPS2", units="", - description="Second Laplace-Lagrange parameter", + description="Second Laplace-Lagrange parameter, ECC*cos(OM)", long_double=True, ) ) diff --git a/src/pint/models/frequency_dependent.py b/src/pint/models/frequency_dependent.py index 7e295a615..4426b69a8 100644 --- a/src/pint/models/frequency_dependent.py +++ b/src/pint/models/frequency_dependent.py @@ -36,7 +36,7 @@ def __init__(self): name="FD1", units="second", value=0.0, - description="Coefficient of frequency-dependent delay", + description="Polynomial coefficient of log-frequency-dependent delay", # descriptionTplt=lambda x: ( # "%d term of frequency" " dependent coefficients" % x # ), diff --git a/src/pint/models/noise_model.py b/src/pint/models/noise_model.py index 455e00230..49036fd8a 100644 --- a/src/pint/models/noise_model.py +++ b/src/pint/models/noise_model.py @@ -301,7 +301,7 @@ def __init__( name="ECORR", units="us", aliases=["TNECORR"], - description="An error term that correlates all TOAs in an observing epoch.", + description="An error term that is correlated among all TOAs in an observing epoch.", ) ) diff --git a/src/pint/models/pulsar_binary.py b/src/pint/models/pulsar_binary.py index 98278784e..b27dc923c 100644 --- a/src/pint/models/pulsar_binary.py +++ b/src/pint/models/pulsar_binary.py @@ -104,7 +104,7 @@ def __init__(self): floatParameter( name="A1", units=ls, - description="Projected semi-major axis of pulsar orbit", + description="Projected semi-major axis of pulsar orbit, ap*sin(i)", ) ) # NOTE: the DOT here takes the value and times 1e-12, tempo/tempo2 can @@ -114,7 +114,7 @@ def __init__(self): name="A1DOT", aliases=["XDOT"], units=ls / u.s, - description="Derivative of projected semi-major axis, da*sin(i)/dt", + description="Derivative of projected semi-major axis, d[ap*sin(i)]/dt", unit_scale=True, scale_factor=1e-12, scale_threshold=1e-7, From 90c4080fb4aad25e6f180cfe35dd08333c736580 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 22 Aug 2023 13:17:09 -0500 Subject: [PATCH 081/144] Setting no longer overrides the name of --- CHANGELOG-unreleased.md | 1 + src/pint/models/timing_model.py | 10 ++++++++++ tests/test_parameters.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 2a67299aa..d45ac7de8 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -25,4 +25,5 @@ the released changes. - Docstrings for `get_toas()` and `get_model_and_toas()` - Set `DelayComponent_list` and `NoiseComponent_list` to empty list if such components are absent - Fix invalid access of `PLANET_SHAPIRO` in models without `Astrometry` +- Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` ### Removed diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index aa73ca579..37f6d16e4 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -498,6 +498,16 @@ def __getattr__(self, name): f"Attribute {name} not found in TimingModel or any Component" ) + def __setattr__(self, name, value): + """Mostly this just sets ``self.name = value``. But in the case where they are both :class:`Parameter` instances + with different names, this copies the ``quantity``, ``uncertainty``, ``frozen`` attributes only. + """ + if isinstance(value, Parameter) and name != value.name: + for p in ["quantity", "uncertainty", "frozen"]: + setattr(getattr(self, name), p, getattr(value, p)) + else: + self.__dict__[name] = value + @property_exists def params_ordered(self): """List of all parameter names in this model and all its components. diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 3a6a7b832..a91e3f206 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -605,3 +605,24 @@ def test_correct_number_of_params_and_FD_terms_after_add_or_remove_param(): m.remove_param("FD4") assert len(m.components["FD"].params) == 3 assert len(m.get_prefix_mapping("FD")) == 3 + + +def test_parameter_retains_name_on_set(): + basic_par_str = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB + """ + + m = get_model(io.StringIO(basic_par_str)) + m.POSEPOCH = m.PEPOCH + assert m.POSEPOCH.name == "POSEPOCH" From 65a2de9dd4aa4d433c4d3a3fd73154845b1775bf Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 22 Aug 2023 13:41:59 -0500 Subject: [PATCH 082/144] deals with prefixParameters; incompatible units fails --- src/pint/models/timing_model.py | 2 +- tests/test_parameters.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 37f6d16e4..9d0b348b8 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -502,7 +502,7 @@ def __setattr__(self, name, value): """Mostly this just sets ``self.name = value``. But in the case where they are both :class:`Parameter` instances with different names, this copies the ``quantity``, ``uncertainty``, ``frozen`` attributes only. """ - if isinstance(value, Parameter) and name != value.name: + if isinstance(value, (Parameter, prefixParameter)) and name != value.name: for p in ["quantity", "uncertainty", "frozen"]: setattr(getattr(self, name), p, getattr(value, p)) else: diff --git a/tests/test_parameters.py b/tests/test_parameters.py index a91e3f206..460bd1ca8 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -626,3 +626,24 @@ def test_parameter_retains_name_on_set(): m = get_model(io.StringIO(basic_par_str)) m.POSEPOCH = m.PEPOCH assert m.POSEPOCH.name == "POSEPOCH" + + +def test_parameter_set_incompatible_fails(): + basic_par_str = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB + """ + + m = get_model(io.StringIO(basic_par_str)) + with pytest.raises(u.core.UnitConversionError): + m.F0 = m.F1 From e8919824ea903bfcdc0dbdfb269c76a45f0a4975 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 22 Aug 2023 13:55:02 -0500 Subject: [PATCH 083/144] inverse translate --- src/pint/utils.py | 96 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index bc50de74f..f1aca7ac6 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -55,6 +55,7 @@ import pint import pint.pulsar_ecliptic from pint.toa_select import TOASelect +from pint.models.parameter import prefixParameter __all__ = [ @@ -1366,11 +1367,39 @@ def _translate_wave_freqs(om, k): return (om * (k + 1)) / (2.0 * np.pi) +def _translate_wavex_freqs(wxfreq, k): + """Use WaveX model WXFREQ_ parameters and wave number k to calculate the Wave model WAVEOM frequency parameter. + + Parameters + ---------- + wxfreq: float or astropy.quantity.Quantity + WaveX frequency from which the WAVEOM parameter will be calculated + If float is given default units of 1/d assigned + k: int + wave number to use to calculate Wave WAVEOM parameter + + Returns + ------- + WAVEOM quantity in units 1/d that can be used in Wave model""" + if isinstance(wxfreq, u.quantity.Quantity): + wxfreq.to(u.d**-1) + else: + wxfreq *= u.d**-1 + if len(wxfreq) == 1: + return (2.0 * np.pi * wxfreq) / (k + 1) + else: + wave_om = [((2.0 * np.pi * wxfreq[i]) / (k[i] + 1)) for i in range(len(wxfreq))] + if np.allclose(wave_om, wave_om[0], atol=1e-3): + return np.mean(wave_om) + else: + return False + + def translate_wave_to_wavex(model): """Go from a Wave model to a WaveX model WaveX frequencies get calculated based on the Wave model WAVEOM parameter and the number of WAVE parameters. - WaveX_freq_k = [WAVEOM * (k+1)] / [2 * pi] + WXFREQ_000k = [WAVEOM * (k+1)] / [2 * pi] WaveX amplitudes are taken from the WAVE pair parameters @@ -1381,9 +1410,7 @@ def translate_wave_to_wavex(model): Returns ------- - indices : list - Indices that have been assigned to new WaveX components - New timing model with Wave model removed and converted WaveX model included""" + New timing model with converted WaveX model included""" from pint.models.wavex import WaveX new_model = deepcopy(model) @@ -1410,6 +1437,35 @@ def translate_wave_to_wavex(model): return new_model +def _add_wave_comp(model, index, amps): + """Tool to add waves to a Wave model for translating between WaveX and Wave model. + + Paramters + --------- + model: pint.models.timing_model.TimingModel + TimingModel containing a Wave model + index: int + Interger label for Wave components. + amps: tuple of float or astropy.quantity.Quantity + Sine and cosine amplitudes + + Returns + ------- + Editted model with WAVE parameter added as a pairwise prefixParameter + """ + model.components["Wave"].add_param( + prefixParameter( + name=f"WAVE{index}", + value=amps, + units="s", + description="Wave components", + type_match="pair", + long_double=True, + parameter_type="pair", + ) + ) + + def get_wavex_freqs(model, index=None): """Return the WaveX frequencies for a timing model. @@ -1502,6 +1558,38 @@ def get_wavex_amps(model, index=None): return values +def translate_wavex_to_wave(model): + """Go from a WaveX timing model to a Wave timing model. + WARNING: Not every WaveX model can be appropriately translated into a Wave model. This is dependent on the user's choice of frequencies in the WaveX model. + In order for a WaveX model to be able to be converted into a Wave model, every WaveX frequency must produce the same value of WAVEOM in the calculation: + + WAVEOM = [2 * pi * WXFREQ_000k] / (k + 1) + Paramters + --------- + model: pint.models.timing_model.TimingModel + TimingModel containing a WaveX model to be converted to a Wave model + + Returns + ------- + New timing model with converted Wave model included""" + from pint.models.wave import Wave + + new_model = deepcopy(model) + indices = model.components["WaveX"].get_indices() + wxfreqs = get_wavex_freqs(model, indices) + wave_om = _translate_wavex_freqs(wxfreqs, indices) + if wave_om == False: + raise ValueError( + "This WaveX model cannot be properly translated into a Wave model due to the WaveX frequencies not producing a consistent WAVEOM value" + ) + amps = get_wavex_amps(model, index=indices) + new_model.remove_component("WaveX") + new_model.add_component(Wave(), validate=False) + new_model.WAVEEPOCH.quantity = model.WXEPOCH.quantity + new_model.WAVE_OM.quantity = wave_om + return new_model + + def weighted_mean(arrin, weights_in, inputmean=None, calcerr=False, sdev=False): """Compute weighted mean of input values From e1150914480b59ce808951382223aaa8432146ba Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 22 Aug 2023 13:56:10 -0500 Subject: [PATCH 084/144] fixed import issue --- src/pint/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index f1aca7ac6..d595d2f47 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -55,7 +55,6 @@ import pint import pint.pulsar_ecliptic from pint.toa_select import TOASelect -from pint.models.parameter import prefixParameter __all__ = [ @@ -1438,6 +1437,8 @@ def translate_wave_to_wavex(model): def _add_wave_comp(model, index, amps): + from pint.models.parameter import prefixParameter + """Tool to add waves to a Wave model for translating between WaveX and Wave model. Paramters From a930c2c660b3892626c814148ee4bc2019ebceb0 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 22 Aug 2023 15:13:39 -0500 Subject: [PATCH 085/144] fixed default behavior of __setattr__ --- src/pint/models/timing_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 9d0b348b8..65a013d54 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -506,7 +506,7 @@ def __setattr__(self, name, value): for p in ["quantity", "uncertainty", "frozen"]: setattr(getattr(self, name), p, getattr(value, p)) else: - self.__dict__[name] = value + super().__setattr__(name, value) @property_exists def params_ordered(self): From 715ad2662c34d688b4f3a424571a6305a973fb93 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 15:20:18 -0500 Subject: [PATCH 086/144] as_latex --- src/pint/models/parameter.py | 32 ++++++++ src/pint/output/publish.py | 152 +++++++++++++++-------------------- 2 files changed, 97 insertions(+), 87 deletions(-) diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index 2b1dd846f..1f7a35609 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -571,6 +571,18 @@ def from_parfile_line(self, line): self.uncertainty = self._set_uncertainty(ucty) return True + def value_as_latex(self): + return f"${self.as_ufloat():.1uSL}$" if not self.frozen else f"{self.value:f}" + + def as_latex(self): + unit_latex = ( + "" + if self.units == "" or self.units is None + else f" ({self.units.to_string(format='latex', fraction=False)})" + ) + value_latex = self.value_as_latex() + return f"{self.name}, {self.description}{unit_latex}", value_latex + def add_alias(self, alias): """Add a name to the list of aliases for this parameter.""" self.aliases.append(alias) @@ -865,6 +877,9 @@ def _set_quantity(self, val): """Convert to string.""" return str(val) + def value_as_latex(self): + return self.value + class boolParameter(Parameter): """Boolean-valued parameter. @@ -932,6 +947,9 @@ def _set_quantity(self, val): return bool(float(val)) return bool(val) + def value_as_latex(self): + return "Y" if self.value else "N" + class intParameter(Parameter): """Integer parameter values. @@ -1000,6 +1018,9 @@ def _set_quantity(self, val): return ival + def value_as_latex(self): + return str(self.value) + class MJDParameter(Parameter): """Parameters for MJD quantities. @@ -1582,6 +1603,9 @@ def name_matches(self, name): def as_parfile_line(self, format="pint"): return self.param_comp.as_parfile_line(format=format) + def as_latex(self): + return self.param_comp.as_latex() + def help_line(self): return self.param_comp.help_line() @@ -1927,6 +1951,14 @@ def as_parfile_line(self, format="pint"): line += " 1" return line + "\n" + def as_latex(self): + unit_latex = ( + "" + if self.units == "" or self.units is None + else f" ({self.units.to_string(format='latex', fraction=False)})" + ) + return f"{self.prefix} {self.key} {' '.join(self.key_value)}, {self.description}{unit_latex}\dotfill & {self.value_as_latex()} \\\\ \n" + def new_param(self, index, copy_all=False): """Create a new but same style mask parameter""" return ( diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index 4e99d1006..e17fdb52e 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -13,11 +13,7 @@ from pint.models.noise_model import NoiseComponent from pint.models.parameter import ( Parameter, - boolParameter, funcParameter, - intParameter, - maskParameter, - strParameter, ) from pint.toa import TOAs from pint.residuals import Residuals, WidebandTOAResiduals @@ -25,35 +21,10 @@ import numpy as np -def publish_param_value(param: Parameter): - """Return LaTeX string for a parameter value""" - if isinstance(param, boolParameter): - return "Y" if param.value else "N" - elif isinstance(param, strParameter): - return param.value - elif isinstance(param, intParameter): - return str(param.value) - else: - return ( - f"${param.as_ufloat():.1uSL}$" if not param.frozen else f"{param.value:f}" - ) - - -def publish_param_unit(param: Parameter): - """Return LaTeX string for a parameter unit""" - return ( - "" - if param.units == "" or param.units is None - else f" ({param.units.to_string(format='latex', fraction=False)})" - ) - - -def publish_param(param): +def publish_param(param: Parameter): """Return LaTeX line for a parameter""" - if isinstance(param, maskParameter): - return f"{param.prefix} {param.key} {' '.join(param.key_value)}, {param.description}{publish_param_unit(param)}\dotfill & {publish_param_value(param)} \\\\ \n" - else: - return f"{param.name}, {param.description}{publish_param_unit(param)}\dotfill & {publish_param_value(param)} \\\\ \n" + label, value = param.as_latex() + return f"{label}\\dotfill & {value} \\\\ \n" def publish( @@ -67,6 +38,7 @@ def publish( include_glitches=False, include_swx=False, include_tzr=False, + include_prefix_summary=True, ): """Generate LaTeX summary of a given timing model and TOAs. @@ -92,6 +64,8 @@ def publish( Whether to include SWX paremeters (default is False) include_tzr: bool Whether to include TZR paremeters (default is False) + include_prefix_summary: bool + Whether to include a summary of prefix and mask parameters (default is True) Returns ------- @@ -191,60 +165,61 @@ def publish( f"Binary model \\dotfill & {model.BINARY.value} \\\\ \n" ) - if "PhaseJump" in model.components: - tex.write( - f"Number of JUMPs \\dotfill & {model.get_number_of_jumps()} \\\\ \n" - ) - - if "DispersionJump" in model.components: - tex.write( - f"Number of DMJUMPs \\dotfill & {len(model.dm_jumps)} \\\\ \n" - ) - - if "DispersionDMX" in model.components: - tex.write( - f"Number of DMX ranges \\dotfill & {len(model.components['DispersionDMX'].get_indices())} \\\\ \n" - ) - - if "SolarWindDispersionX" in model.components: - tex.write( - f"Number of SWX ranges \\dotfill & {len(model.components['SolarWindDispersionX'].get_indices())} \\\\ \n" - ) - - if "Glitch" in model.components: - tex.write( - f"Number of Glitches \\dotfill & {len(model.components['Glitch'].glitch_indices)} \\\\ \n" - ) - - if "FD" in model.components: - tex.write( - f"Number of FD parameters \\dotfill & {model.num_FD_terms} \\\\ \n" - ) - - if "ScaleToaError" in model.components: - tex.write( - f"Number of EFACs \\dotfill & {len(model.EFACs)} \\\\ \n" - ) - tex.write( - f"Number of EQUADs \\dotfill & {len(model.EQUADs)} \\\\ \n" - ) - - if "EcorrNoise" in model.components: - tex.write( - f"Number of ECORRs \\dotfill & {len(model.ECORRs)} \\\\ \n" - ) - - if "ScaleDmError" in model.components: - tex.write( - f"Number of DMEFACs \\dotfill & {len(model.DMEFACs)} \\\\ \n" - ) - tex.write( - f"Number of DMEQUADs \\dotfill & {len(model.DMEQUADs)} \\\\ \n" - ) - if "Wave" in model.components: - tex.write( - f"Number of WAVE components \\dotfill & {model.num_wave_terms} \\\\ \n" - ) + if include_prefix_summary: + if "PhaseJump" in model.components: + tex.write( + f"Number of JUMPs \\dotfill & {model.get_number_of_jumps()} \\\\ \n" + ) + + if "DispersionJump" in model.components: + tex.write( + f"Number of DMJUMPs \\dotfill & {len(model.dm_jumps)} \\\\ \n" + ) + + if "DispersionDMX" in model.components: + tex.write( + f"Number of DMX ranges \\dotfill & {len(model.components['DispersionDMX'].get_indices())} \\\\ \n" + ) + + if "SolarWindDispersionX" in model.components: + tex.write( + f"Number of SWX ranges \\dotfill & {len(model.components['SolarWindDispersionX'].get_indices())} \\\\ \n" + ) + + if "Glitch" in model.components: + tex.write( + f"Number of Glitches \\dotfill & {len(model.components['Glitch'].glitch_indices)} \\\\ \n" + ) + + if "FD" in model.components: + tex.write( + f"Number of FD parameters \\dotfill & {model.num_FD_terms} \\\\ \n" + ) + + if "ScaleToaError" in model.components: + tex.write( + f"Number of EFACs \\dotfill & {len(model.EFACs)} \\\\ \n" + ) + tex.write( + f"Number of EQUADs \\dotfill & {len(model.EQUADs)} \\\\ \n" + ) + + if "EcorrNoise" in model.components: + tex.write( + f"Number of ECORRs \\dotfill & {len(model.ECORRs)} \\\\ \n" + ) + + if "ScaleDmError" in model.components: + tex.write( + f"Number of DMEFACs \\dotfill & {len(model.DMEFACs)} \\\\ \n" + ) + tex.write( + f"Number of DMEQUADs \\dotfill & {len(model.DMEQUADs)} \\\\ \n" + ) + if "Wave" in model.components: + tex.write( + f"Number of WAVE components \\dotfill & {model.num_wave_terms} \\\\ \n" + ) tex.write("\\hline\n") @@ -303,7 +278,10 @@ def publish( tex.write("\\hline\n") derived_params = [ - p for p in model.params if isinstance(getattr(model, p), funcParameter) + getattr(model, p) + for p in model.params + if isinstance(getattr(model, p), funcParameter) + and getattr(model, p).quantity is not None ] if len(derived_params) > 0: tex.write("\multicolumn{2}{c}{Derived Quantities} \\\\ \n") From 42b0beb44655a741e27c551b849ec096b13fe5e1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 15:22:10 -0500 Subject: [PATCH 087/144] .gitignore --- .gitignore | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b76b751df..64a407117 100644 --- a/.gitignore +++ b/.gitignore @@ -119,4 +119,10 @@ docs/examples/*.ipynb docs/examples-rendered/*.py # VSCode wants to put virtualenvs here -.env \ No newline at end of file +.env + +# pintpublish output +*.tex +*.aux +*.log +*.pdf \ No newline at end of file From a17e5adeb9ca91c801c2dbfc2ebc400310ca594b Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 22 Aug 2023 15:29:00 -0500 Subject: [PATCH 088/144] options --- src/pint/output/publish.py | 111 ++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/pint/output/publish.py b/src/pint/output/publish.py index e17fdb52e..2cb82b0ac 100644 --- a/src/pint/output/publish.py +++ b/src/pint/output/publish.py @@ -39,6 +39,9 @@ def publish( include_swx=False, include_tzr=False, include_prefix_summary=True, + include_set_params=True, + include_derived_params=True, + include_fit_summary=True, ): """Generate LaTeX summary of a given timing model and TOAs. @@ -66,6 +69,12 @@ def publish( Whether to include TZR paremeters (default is False) include_prefix_summary: bool Whether to include a summary of prefix and mask parameters (default is True) + include_set_params: bool + Whether to include set params (default is True) + include_derived_params: bool + Whether to include derived params (default is True) + include_fit_summary: bool + Whether to include fit summary params (default is True) Returns ------- @@ -223,29 +232,32 @@ def publish( tex.write("\\hline\n") - tex.write("\\multicolumn{2}{c}{Fit summary}\\\\ \n") - tex.write("\\hline\n") - tex.write( - f"Number of free parameters \\dotfill & {len(model.free_params)} \\\\ \n" - ) - tex.write(f"Fitting method \\dotfill & {fit_method} \\\\ \n") - tex.write( - f"RMS TOA residuals ($\\mu s$) \\dotfill & {toares.calc_time_resids().to('us').value.std():.2f} \\\\ \n" - ) - if toas.is_wideband(): + if include_fit_summary: + tex.write("\\multicolumn{2}{c}{Fit summary}\\\\ \n") + tex.write("\\hline\n") tex.write( - f"RMS DM residuals (pc / cm3) \\dotfill & {dmres.calc_resids().to('pc/cm^3').value.std():.2f} \\\\ \n" + f"Number of free parameters \\dotfill & {len(model.free_params)} \\\\ \n" ) - tex.write( - f"$\\chi^2$ \\dotfill & {res.chi2:.2f} \\\\ \n" - ) - if toas.is_wideband(): - tex.write(f"Degrees of freedom \\dotfill & {res.dof} \\\\ \n") - else: tex.write( - f"Reduced $\\chi^2$ \\dotfill & {res.reduced_chi2:.2f} \\\\ \n" + f"Fitting method \\dotfill & {fit_method} \\\\ \n" ) - tex.write("\\hline\n") + tex.write( + f"RMS TOA residuals ($\\mu s$) \\dotfill & {toares.calc_time_resids().to('us').value.std():.2f} \\\\ \n" + ) + if toas.is_wideband(): + tex.write( + f"RMS DM residuals (pc / cm3) \\dotfill & {dmres.calc_resids().to('pc/cm^3').value.std():.2f} \\\\ \n" + ) + tex.write( + f"$\\chi^2$ \\dotfill & {res.chi2:.2f} \\\\ \n" + ) + if toas.is_wideband(): + tex.write(f"Degrees of freedom \\dotfill & {res.dof} \\\\ \n") + else: + tex.write( + f"Reduced $\\chi^2$ \\dotfill & {res.reduced_chi2:.2f} \\\\ \n" + ) + tex.write("\\hline\n") tex.write("\multicolumn{2}{c}{Measured Quantities} \\\\ \n") tex.write("\\hline\n") @@ -260,36 +272,43 @@ def publish( tex.write("\\hline\n") - tex.write("\multicolumn{2}{c}{Set Quantities} \\\\ \n") - tex.write("\\hline\n") - for p in model.params: - param = getattr(model, p) - - if ( - all([not isinstance(param._parent, exc) for exc in exclude_components]) - and param.value is not None - and param.frozen - and p not in exclude_params - and (param.value != 0 or include_zeros) - and not isinstance(param, funcParameter) - ): - tex.write(publish_param(param)) - - tex.write("\\hline\n") - - derived_params = [ - getattr(model, p) - for p in model.params - if isinstance(getattr(model, p), funcParameter) - and getattr(model, p).quantity is not None - ] - if len(derived_params) > 0: - tex.write("\multicolumn{2}{c}{Derived Quantities} \\\\ \n") + if include_set_params: + tex.write("\multicolumn{2}{c}{Set Quantities} \\\\ \n") tex.write("\\hline\n") - for param in derived_params: - tex.write(publish_param(param)) + for p in model.params: + param = getattr(model, p) + + if ( + all( + [ + not isinstance(param._parent, exc) + for exc in exclude_components + ] + ) + and param.value is not None + and param.frozen + and p not in exclude_params + and (param.value != 0 or include_zeros) + and not isinstance(param, funcParameter) + ): + tex.write(publish_param(param)) + tex.write("\\hline\n") + if include_derived_params: + derived_params = [ + getattr(model, p) + for p in model.params + if isinstance(getattr(model, p), funcParameter) + and getattr(model, p).quantity is not None + ] + if len(derived_params) > 0: + tex.write("\multicolumn{2}{c}{Derived Quantities} \\\\ \n") + tex.write("\\hline\n") + for param in derived_params: + tex.write(publish_param(param)) + tex.write("\\hline\n") + tex.write("\\end{tabular}\n") tex.write("\\end{table}\n") tex.write("\\end{document}\n") From d32b9655aa72c78a6fe987dd7d7de08b9a90b829 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 22 Aug 2023 16:16:56 -0500 Subject: [PATCH 089/144] inverse translate function with additional fixes --- src/pint/utils.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index d595d2f47..4a7bbaa81 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1385,11 +1385,14 @@ def _translate_wavex_freqs(wxfreq, k): else: wxfreq *= u.d**-1 if len(wxfreq) == 1: - return (2.0 * np.pi * wxfreq) / (k + 1) + return (2.0 * np.pi * wxfreq) / (k + 1.0) else: - wave_om = [((2.0 * np.pi * wxfreq[i]) / (k[i] + 1)) for i in range(len(wxfreq))] - if np.allclose(wave_om, wave_om[0], atol=1e-3): - return np.mean(wave_om) + wave_om = [ + ((2.0 * np.pi * wxfreq[i]) / (k[i] + 1.0)) for i in range(len(wxfreq)) + ] + if np.allclose(wave_om, wave_om[0], atol=1e-2): + om = sum(wave_om) / len(wave_om) + return om else: return False @@ -1424,8 +1427,8 @@ def translate_wave_to_wavex(model): new_model.WXEPOCH.value = wave_epoch.value for k, wave_term in enumerate(wave_terms): wave_sin_amp, wave_cos_amp = wave_term.quantity - wavex_freq = _translate_wave_freqs(wave_om, k) - if k == 1: + wavex_freq = _translate_wave_freqs(wave_om, (k + 1)) + if k == 0: new_model.WXFREQ_0001.value = wavex_freq.value new_model.WXSIN_0001.value = -wave_sin_amp.value new_model.WXCOS_0001.value = -wave_cos_amp.value @@ -1467,7 +1470,7 @@ def _add_wave_comp(model, index, amps): ) -def get_wavex_freqs(model, index=None): +def get_wavex_freqs(model, index=None, quantity=False): """Return the WaveX frequencies for a timing model. If index is specified, returns the frequencies corresponding to the user-provided indices. @@ -1479,11 +1482,13 @@ def get_wavex_freqs(model, index=None): Timing model from which to return WaveX frequencies index: : float, int, list, np.ndarray, None Number or list/array of numbers corresponding to WaveX frequencies to return + quantity: bool + If set to True, returns a list of astropy.quanitity.Quantity rather than a list of prefixParameters Returns ------- List of WXFREQ_ parameters""" - if index == None: + if index is None: freqs = model.components["WaveX"].get_prefix_mapping_component("WXFREQ_") if len(freqs) == 1: values = getattr(model.components["WaveX"], freqs.values()) @@ -1501,10 +1506,15 @@ def get_wavex_freqs(model, index=None): raise TypeError( f"index most be a float, int, set, list, array, or None - not {type(index)}" ) + if quantity: + if len(values) == 1: + values = values.quantity + else: + values = [v.quantity for v in values] return values -def get_wavex_amps(model, index=None): +def get_wavex_amps(model, index=None, quantity=False): """Return the WaveX amplitudes for a timing model. If index is specified, returns the sine/cosine amplitudes corresponding to the user-provided indices. @@ -1516,11 +1526,13 @@ def get_wavex_amps(model, index=None): Timing model from which to return WaveX frequencies index: : float, int, list, np.ndarray, None Number or list/array of numbers corresponding to WaveX amplitudes to return + quantity: bool + If set to True, returns a list of tuples of astropy.quanitity.Quantity rather than a list of prefixParameters tuples Returns ------- List of WXSIN_ and WXCOS_ parameters""" - if index == None: + if index is None: indices = ( model.components["WaveX"].get_prefix_mapping_component("WXSIN_").keys() ) @@ -1556,6 +1568,11 @@ def get_wavex_amps(model, index=None): raise TypeError( f"index most be a float, int, set, list, array, or None - not {type(index)}" ) + if quantity: + if isinstance(values, tuple): + values = tuple(v.quantity for v in values) + if isinstance(values, list): + values = [tuple((v[0].quantity, v[1].quantity)) for v in values] return values @@ -1577,17 +1594,22 @@ def translate_wavex_to_wave(model): new_model = deepcopy(model) indices = model.components["WaveX"].get_indices() - wxfreqs = get_wavex_freqs(model, indices) + wxfreqs = get_wavex_freqs(model, indices, quantity=True) wave_om = _translate_wavex_freqs(wxfreqs, indices) if wave_om == False: raise ValueError( "This WaveX model cannot be properly translated into a Wave model due to the WaveX frequencies not producing a consistent WAVEOM value" ) - amps = get_wavex_amps(model, index=indices) + wave_amps = get_wavex_amps(model, index=indices, quantity=True) new_model.remove_component("WaveX") new_model.add_component(Wave(), validate=False) new_model.WAVEEPOCH.quantity = model.WXEPOCH.quantity new_model.WAVE_OM.quantity = wave_om + if len(indices) == 1: + _add_wave_comp(new_model, index=indices, amps=wave_amps) + else: + for i in range(len(indices)): + _add_wave_comp(new_model, index=indices[i], amps=wave_amps[i]) return new_model From debc69e3e53656fa6a7f6009dda5af65fe1b147f Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 22 Aug 2023 16:18:00 -0500 Subject: [PATCH 090/144] changed atol for inverse translate --- src/pint/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index 4a7bbaa81..096872f93 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1390,7 +1390,7 @@ def _translate_wavex_freqs(wxfreq, k): wave_om = [ ((2.0 * np.pi * wxfreq[i]) / (k[i] + 1.0)) for i in range(len(wxfreq)) ] - if np.allclose(wave_om, wave_om[0], atol=1e-2): + if np.allclose(wave_om, wave_om[0], atol=1e-3): om = sum(wave_om) / len(wave_om) return om else: From 0448eec788025bf0b536008a2c9f9b84293b7d0b Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 22 Aug 2023 16:26:33 -0500 Subject: [PATCH 091/144] Fixed negative amplitudes --- src/pint/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 096872f93..c51417dad 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1606,9 +1606,11 @@ def translate_wavex_to_wave(model): new_model.WAVEEPOCH.quantity = model.WXEPOCH.quantity new_model.WAVE_OM.quantity = wave_om if len(indices) == 1: + wave_amps = tuple(w * -1.0 for w in wave_amps) _add_wave_comp(new_model, index=indices, amps=wave_amps) else: for i in range(len(indices)): + wave_amps[i] = tuple(w * -1.0 for w in wave_amps[i]) _add_wave_comp(new_model, index=indices[i], amps=wave_amps[i]) return new_model From cb55db71c07637714bb17cea0ab4c56482eab551 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 22 Aug 2023 16:44:46 -0500 Subject: [PATCH 092/144] Fixed wave number error --- src/pint/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index c51417dad..33202d7b2 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1427,7 +1427,7 @@ def translate_wave_to_wavex(model): new_model.WXEPOCH.value = wave_epoch.value for k, wave_term in enumerate(wave_terms): wave_sin_amp, wave_cos_amp = wave_term.quantity - wavex_freq = _translate_wave_freqs(wave_om, (k + 1)) + wavex_freq = _translate_wave_freqs(wave_om, k) if k == 0: new_model.WXFREQ_0001.value = wavex_freq.value new_model.WXSIN_0001.value = -wave_sin_amp.value @@ -1595,7 +1595,7 @@ def translate_wavex_to_wave(model): new_model = deepcopy(model) indices = model.components["WaveX"].get_indices() wxfreqs = get_wavex_freqs(model, indices, quantity=True) - wave_om = _translate_wavex_freqs(wxfreqs, indices) + wave_om = _translate_wavex_freqs(wxfreqs, (indices - 1)) if wave_om == False: raise ValueError( "This WaveX model cannot be properly translated into a Wave model due to the WaveX frequencies not producing a consistent WAVEOM value" From 80579c6b9c26d567e176f0eb87cb5750009d9847 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Wed, 23 Aug 2023 11:11:38 -0500 Subject: [PATCH 093/144] fix as_latex --- CHANGELOG-unreleased.md | 1 + src/pint/models/parameter.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index ad4c097d4..2e1a3f678 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -19,6 +19,7 @@ the released changes. - `pint.print_info()` function for bug reporting - Added an autocorrelation function to check for chain convergence in `event_optimize` - Minor doc updates to explain default NHARMS and missing derivative functions +- `Parameter.as_latex` method for latex representation of a parameter. - `pint.output.publish` module and `pintpublish` script for generating publication (LaTeX) output. ### Fixed - Deleting JUMP1 from flag tables will not prevent fitting diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index 1f7a35609..eb51e0db8 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -1957,7 +1957,10 @@ def as_latex(self): if self.units == "" or self.units is None else f" ({self.units.to_string(format='latex', fraction=False)})" ) - return f"{self.prefix} {self.key} {' '.join(self.key_value)}, {self.description}{unit_latex}\dotfill & {self.value_as_latex()} \\\\ \n" + return ( + f"{self.prefix} {self.key} {' '.join(self.key_value)}, {self.description}{unit_latex}", + self.value_as_latex(), + ) def new_param(self, index, copy_all=False): """Create a new but same style mask parameter""" From ab589ff2d773b30f9fa30e46f96372b60e4a47b3 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Wed, 23 Aug 2023 12:15:27 -0500 Subject: [PATCH 094/144] deal with old astropy --- src/pint/models/parameter.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index eb51e0db8..9a3e5b3d9 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -1952,11 +1952,19 @@ def as_parfile_line(self, format="pint"): return line + "\n" def as_latex(self): - unit_latex = ( - "" - if self.units == "" or self.units is None - else f" ({self.units.to_string(format='latex', fraction=False)})" - ) + try: + unit_latex = ( + "" + if self.units == "" or self.units is None + else f" ({self.units.to_string(format='latex', fraction=False)})" + ) + except TypeError: + # `fraction` option is not available in old astropy versions. + unit_latex = ( + "" + if self.units == "" or self.units is None + else f" ({self.units.to_string(format='latex')})" + ) return ( f"{self.prefix} {self.key} {' '.join(self.key_value)}, {self.description}{unit_latex}", self.value_as_latex(), From e6a28d9b72689c82221763c23c0741ae7b1190a3 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 24 Aug 2023 09:21:57 -0500 Subject: [PATCH 095/144] old astropy fix --- src/pint/models/parameter.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index 9a3e5b3d9..5b2c2e36d 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -575,11 +575,19 @@ def value_as_latex(self): return f"${self.as_ufloat():.1uSL}$" if not self.frozen else f"{self.value:f}" def as_latex(self): - unit_latex = ( - "" - if self.units == "" or self.units is None - else f" ({self.units.to_string(format='latex', fraction=False)})" - ) + try: + unit_latex = ( + "" + if self.units == "" or self.units is None + else f" ({self.units.to_string(format='latex', fraction=False)})" + ) + except TypeError: + # to deal with old astropy + unit_latex = ( + "" + if self.units == "" or self.units is None + else f" ({self.units.to_string(format='latex')})" + ) value_latex = self.value_as_latex() return f"{self.name}, {self.description}{unit_latex}", value_latex From b0ad68caa4e7afb44a3fea930bed86a5e4eb0f05 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 24 Aug 2023 12:17:49 -0500 Subject: [PATCH 096/144] Fixing errors --- src/pint/models/wave.py | 19 +++++++++---------- src/pint/utils.py | 9 +++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pint/models/wave.py b/src/pint/models/wave.py index 7abe4ed79..1ceb2b9e1 100644 --- a/src/pint/models/wave.py +++ b/src/pint/models/wave.py @@ -28,7 +28,13 @@ class Wave(PhaseComponent): def __init__(self): super().__init__() - + self.add_param( + MJDParameter( + name="WAVEEPOCH", + description="Reference epoch for wave solution", + time_scale="tdb", + ) + ) self.add_param( floatParameter( name="WAVE_OM", @@ -46,13 +52,6 @@ def __init__(self): parameter_type="pair", ) ) - self.add_param( - MJDParameter( - name="WAVEEPOCH", - description="Reference epoch for wave solution", - time_scale="tdb", - ) - ) self.phase_funcs_component += [self.wave_phase] def setup(self): @@ -64,14 +63,14 @@ def validate(self): super().validate() self.setup() if self.WAVEEPOCH.quantity is None: - if self.PEPOCH.quantity is None: + if self._parent.PEPOCH.quantity is None: raise MissingParameter( "Wave", "WAVEEPOCH", "WAVEEPOCH or PEPOCH are required if " "WAVE_OM is set.", ) else: - self.WAVEEPOCH = self.PEPOCH + self.WAVEEPOCH.quantity = self._parent.PEPOCH.quantity if (not hasattr(self._parent, "F0")) or (self._parent.F0.quantity is None): raise MissingParameter( diff --git a/src/pint/utils.py b/src/pint/utils.py index 33202d7b2..dd27096ec 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1508,7 +1508,7 @@ def get_wavex_freqs(model, index=None, quantity=False): ) if quantity: if len(values) == 1: - values = values.quantity + values = [values[0].quantity] else: values = [v.quantity for v in values] return values @@ -1602,14 +1602,15 @@ def translate_wavex_to_wave(model): ) wave_amps = get_wavex_amps(model, index=indices, quantity=True) new_model.remove_component("WaveX") - new_model.add_component(Wave(), validate=False) + new_model.add_component(Wave()) new_model.WAVEEPOCH.quantity = model.WXEPOCH.quantity new_model.WAVE_OM.quantity = wave_om if len(indices) == 1: - wave_amps = tuple(w * -1.0 for w in wave_amps) - _add_wave_comp(new_model, index=indices, amps=wave_amps) + wave_amps = tuple(w * -1.0 for w in wave_amps[0]) + _add_wave_comp(new_model, index=indices[0], amps=wave_amps) else: for i in range(len(indices)): + print(wave_amps[i]) wave_amps[i] = tuple(w * -1.0 for w in wave_amps[i]) _add_wave_comp(new_model, index=indices[i], amps=wave_amps[i]) return new_model From 6648c4a88666a2cc8f5e00931617e97db56a0d16 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 24 Aug 2023 14:34:42 -0500 Subject: [PATCH 097/144] -- --- .readthedocs.yml | 5 +++-- requirements_dev.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 391de3894..4bfc807fc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,7 +17,8 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: - configuration: docs/conf.py + builder: html + configuration: docs/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF formats: @@ -29,4 +30,4 @@ python: - requirements: requirements_dev.txt - requirements: requirements.txt - method: pip - path: . \ No newline at end of file + path: . diff --git a/requirements_dev.txt b/requirements_dev.txt index 54cdeeab1..0f8816b6f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,7 +3,7 @@ pip>=9.0.1 setuptools>=41.0 coverage>=4.3.4 traitlets -Sphinx>=2.2,!=5.1.0 +Sphinx>=2.2,!=5.1.0,<7.0 astropy>=4.0,!=4.0.1,!=4.0.1.post1 #astropy-helpers>=1.3 sphinx-rtd-theme>=1.1.1 From 88cbecbe17ef8c83ea4b293369d57d002bddbddd Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 25 Aug 2023 12:55:26 -0500 Subject: [PATCH 098/144] Changes to facilitate inverse translate --- src/pint/models/wave.py | 46 ++++++++++++++++++++++++++++++++++++++++ src/pint/models/wavex.py | 8 +------ src/pint/utils.py | 43 ++++++------------------------------- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/pint/models/wave.py b/src/pint/models/wave.py index 1ceb2b9e1..2092833a5 100644 --- a/src/pint/models/wave.py +++ b/src/pint/models/wave.py @@ -94,6 +94,52 @@ def print_par(self, format="pint"): return result + def add_wave_component(self, amps, index=None): + """Add Wave Component + + Parameters + ---------- + + index : int + Interger label for Wave components. + amps : tuple of float or astropy.quantity.Quantity + Sine and cosine amplitudes + + Returns + ------- + + index : + Index that has been addigned to new Wave component + """ + #### If index is None, increment the current max Wave index by 1. Increment using WAVE + if index is None: + dct = self.get_prefix_mapping_component("WAVE") + index = np.max(list(dct.keys())) + 1 + i = f"{int(index):04d}" + + if int(index) in self.get_prefix_mapping_component("WAVE"): + raise ValueError( + f"Index '{index}' is already in use in this model. Please choose another" + ) + + for amp in amps: + if isinstance(amp, u.quantity.Quantity): + amp = amp.to_value(u.s) + self.add_param( + prefixParameter( + name=f"WAVE{index}", + value=amps, + units="s", + description="Wave components", + type_match="pair", + long_double=True, + parameter_type="pair", + ) + ) + self.setup() + self.validate() + return index + def wave_phase(self, toas, delays): times = 0 wave_names = ["WAVE%d" % ii for ii in range(1, self.num_wave_terms + 1)] diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 9856ee5f3..1ba3e54ff 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -39,8 +39,6 @@ def __init__(self): self.set_special_params(["WXFREQ_0001", "WXSIN_0001", "WXCOS_0001"]) self.delay_funcs_component += [self.wavex_delay] - # Register derivative functions PLACEHOLDER - def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True): """Add WaveX component @@ -81,7 +79,6 @@ def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True) if isinstance(wxcos, u.quantity.Quantity): wxcos = wxcos.to_value(u.s) if isinstance(wxfreq, u.quantity.Quantity): - # wxfreq = wxfreq.value wxfreq = wxfreq.to_value(1 / u.d) self.add_param( prefixParameter( @@ -262,9 +259,7 @@ def get_indices(self): # Initialize setup def setup(self): super().setup() - - # Get WaveX mapping - # Register WXSIN and WXCOS derivatives PLACEHOLDER + # Get WaveX mapping and register WXSIN and WXCOS derivatives for prefix_par in self.get_params_of_type("prefixParameter"): if prefix_par.startswith("WXSIN_"): self.register_deriv_funcs(self.d_wavex_delay_d_WXSIN, prefix_par) @@ -281,7 +276,6 @@ def validate(self): WXSIN_mapping = self.get_prefix_mapping_component("WXSIN_") WXCOS_mapping = self.get_prefix_mapping_component("WXCOS_") if WXFREQ_mapping.keys() != WXSIN_mapping.keys(): - # PLACEHOLDER : Report the mismatched parameters raise ValueError( "WXFREQ_ parameters do not match WXSIN_ parameters." "Please check your prefixed parameters" diff --git a/src/pint/utils.py b/src/pint/utils.py index dd27096ec..03b28667f 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1439,37 +1439,6 @@ def translate_wave_to_wavex(model): return new_model -def _add_wave_comp(model, index, amps): - from pint.models.parameter import prefixParameter - - """Tool to add waves to a Wave model for translating between WaveX and Wave model. - - Paramters - --------- - model: pint.models.timing_model.TimingModel - TimingModel containing a Wave model - index: int - Interger label for Wave components. - amps: tuple of float or astropy.quantity.Quantity - Sine and cosine amplitudes - - Returns - ------- - Editted model with WAVE parameter added as a pairwise prefixParameter - """ - model.components["Wave"].add_param( - prefixParameter( - name=f"WAVE{index}", - value=amps, - units="s", - description="Wave components", - type_match="pair", - long_double=True, - parameter_type="pair", - ) - ) - - def get_wavex_freqs(model, index=None, quantity=False): """Return the WaveX frequencies for a timing model. @@ -1605,14 +1574,14 @@ def translate_wavex_to_wave(model): new_model.add_component(Wave()) new_model.WAVEEPOCH.quantity = model.WXEPOCH.quantity new_model.WAVE_OM.quantity = wave_om - if len(indices) == 1: - wave_amps = tuple(w * -1.0 for w in wave_amps[0]) - _add_wave_comp(new_model, index=indices[0], amps=wave_amps) - else: - for i in range(len(indices)): + new_model.WAVE1.quantity = tuple(w * -1.0 for w in wave_amps[0]) + if len(indices) > 1: + for i in range(1, len(indices)): print(wave_amps[i]) wave_amps[i] = tuple(w * -1.0 for w in wave_amps[i]) - _add_wave_comp(new_model, index=indices[i], amps=wave_amps[i]) + new_model.components["Wave"].add_wave_component( + wave_amps[i], index=indices[i] + ) return new_model From 2eccfe78cad27d821a778599bf3e1052689a4d67 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 25 Aug 2023 14:38:05 -0500 Subject: [PATCH 099/144] Roundtrip test and minor edits --- src/pint/models/wavex.py | 18 +++++++++++++++++- src/pint/utils.py | 30 +++++++++++++++++++++++------- tests/test_wavex.py | 19 +++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 1ba3e54ff..f8d57a2db 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -20,7 +20,23 @@ class WaveX(DelayComponent): .. paramtable:: :class: pint.models.wavex.WaveX - This is an extension of the L13 method described in Lentati et al., 2013 + This is an extension of the L13 method described in Lentati et al., 2013 doi: 10.1103/PhysRevD.87.104021 + This model is similar to the TEMPO2 WAVE model parameters and users can convert a TimingModel with a Wave model + to a WaveX model and produce the same results. The main differences are that the WaveX frequencies are explicitly stated, + they do not necessarily need to be harmonics of some base frequency, the wave amplitudes are fittable parameters, and the + sine and cosine amplutides are reported as separate prefixParameters rather than as a single pairParameter. + + Analogous parameters in both models have the same units: + WAVEEPOCH is the same as WXEPOCH + WAVEOM and WXFREQ_000N have units of 1/d + WAVEN and WXSIN_000N/WXCOS_000N have units of seconds + + The pint.utils functions translate_wave_to_wavex() and translate_wavex_to_wave() can be used to go back and forth between + two model. WARNING: If the choice of WaveX frequencies in a TimingModel doesn't correspond to harmonics of some base + freqeuncy, it will not be possible to convert it to a Wave model. + + To set up a WaveX model, users can use the pint.utils function wavex_setup() with either a list of frequencies or a choice + of harmonics of a base frequency determined by 2 * pi /Timespan """ register = True diff --git a/src/pint/utils.py b/src/pint/utils.py index 03b28667f..4b44fd8fa 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -58,6 +58,9 @@ __all__ = [ + "PINTPrecisionError", + "check_longdouble_precision", + "require_longdouble_precision", "PosVel", "numeric_partial", "numeric_partials", @@ -71,26 +74,39 @@ "lines_of", "interesting_lines", "pmtot", - "dmxselections", - "dmxparse", - "dmxstats", + "dmxrange", + "sum_print", "dmx_ranges_old", "dmx_ranges", + "dmxselections", + "dmxstats", + "dmxparse", + "get_prefix_timerange", + "get_prefix_timeranges", + "find_prefix_bytime", + "merge_dmx", + "split_dmx", + "split_swx", + "wavex_setup", + "translate_wave_to_wavex", + "get_wavex_freqs", + "get_wavex_amps", + "translate_wavex_to_wave", "weighted_mean", "ELL1_check", "FTest", "add_dummy_distance", "remove_dummy_distance", "info_string", - "print_color_examples", + "list_parameters", "colorize", + "print_color_examples", "group_iterator", "compute_hash", - "PINTPrecisionError", - "check_longdouble_precision", - "require_longdouble_precision", "get_conjunction", "divide_times", + "convert_dispersion_measure", + "parse_time", "get_unit", ] diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 247a2e2eb..b066e6d7e 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -59,6 +59,11 @@ WXSIN_0003 3 WXCOS_0003 3 """ +wave_par = """ + WAVEEPOCH 55321.000000 + WAVE_OM 0.1 + WAVE1 0.2 0.1 + WAVE2 0.6 0.3""" def wavex_delay(waves, toas): @@ -286,3 +291,17 @@ def test_multiple_wavex_explicit_indices_duplicate(): indices = model.components["WaveX"].add_wavex_components( [0.2, 0.3], indices=[1, 3], wxsins=[2, 3], wxcoses=[2, 3] ) + + +def test_wave_wavex_roundtrip_conversion(): + # Check that when starting with a TimingModel with a Wave model, conversion to a WaveX mode and then back produces consistent results + model = get_model(StringIO(par1)) + toas = make_fake_toas_uniform(55000, 55100, 500, model, obs="gbt") + wave_model = get_model(StringIO(par1 + wave_par)) + wave_to_wavex_model = pint.utils.translate_wave_to_wavex(wave_model) + wavex_to_wave_model = pint.utils.translate_wavex_to_wave(wave_to_wavex_model) + rs_wave = Residuals(toas, wave_model) + rs_wave_to_wavex = Residuals(toas, wave_to_wavex_model) + rs_wavex_to_wave = Residuals(toas, wavex_to_wave_model) + assert np.allclose(rs_wave.resids, rs_wave_to_wavex.resids, atol=1e-3) + assert np.allclose(rs_wave.resids, rs_wavex_to_wave.resids, atol=1e-3) From 5521f49bbf0ab161aac66b7656ce3bd2a7f2222e Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Fri, 25 Aug 2023 14:56:56 -0500 Subject: [PATCH 100/144] Editted comments --- src/pint/models/wavex.py | 18 ++++++---- src/pint/utils.py | 77 +++++++++++++++++++++++++--------------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index f8d57a2db..d45b9cc7e 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -9,7 +9,8 @@ class WaveX(DelayComponent): - """Implementation of the wave model as a delay correction + """ + Implementation of the wave model as a delay correction Delays are expressed as a sum of sinusoids. @@ -56,7 +57,8 @@ def __init__(self): self.delay_funcs_component += [self.wavex_delay] def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True): - """Add WaveX component + """ + Add WaveX component Parameters ---------- @@ -132,7 +134,8 @@ def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True) def add_wavex_components( self, wxfreqs, indices=None, wxsins=0, wxcoses=0, frozens=True ): - """Add WaveX components with specified base frequencies + """ + Add WaveX components with specified base frequencies Parameters ---------- @@ -147,6 +150,7 @@ def add_wavex_components( Cosine amplitudes for WaveX components frozens : iterable of bool or bool Indicates whether sine adn cosine amplitudes of wavex components will be fit + Returns ------- @@ -239,7 +243,8 @@ def add_wavex_components( return added_indices def remove_wavex_component(self, index): - """Remove all WaveX components associated with a given index or list of indices + """ + Remove all WaveX components associated with a given index or list of indices Parameters ---------- @@ -262,7 +267,8 @@ def remove_wavex_component(self, index): self.validate() def get_indices(self): - """Returns an array of intergers corresponding to WaveX component parameters using WXFREQs + """ + Returns an array of intergers corresponding to WaveX component parameters using WXFREQs Returns ------- @@ -285,7 +291,7 @@ def setup(self): self.num_wave_freqs = len(self.wave_freqs) def validate(self): - """Validate all the WaveX parameters""" + # Validate all the WaveX parameters super().validate() self.setup() WXFREQ_mapping = self.get_prefix_mapping_component("WXFREQ_") diff --git a/src/pint/utils.py b/src/pint/utils.py index 4b44fd8fa..a5a8c1e05 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1285,7 +1285,8 @@ def split_swx(model, time): def wavex_setup(model, T_span, freqs=None, n_freqs=None): - """Set-up a WaveX model based on either an array of user-provided frequencies or the wave number + """ + Set-up a WaveX model based on either an array of user-provided frequencies or the wave number frequency calculation. Sine and Cosine amplitudes are initially set to zero User specifies T_span and either freqs or n_freqs. This function assumes that the timing model does not already @@ -1294,19 +1295,21 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): Parameters ---------- - model: pint.models.timing_model.TimingModel - freqs: iterable of float or astropy.quantity.Quantity, None + + model : pint.models.timing_model.TimingModel + freqs : iterable of float or astropy.quantity.Quantity, None User inputed base frequencies - n_freqs: int, None + n_freqs : int, None Number of wave frequencies to calculate using the equation: freq_n = 2 * pi * n / T_span Where n is the wave number, and T_span is the total time span of the toas in the fitter object - T_span: float, astropy.quantity.Quantity + T_span : float, astropy.quantity.Quantity Time span used to calculate nyquist frequency when using freqs Time span used to calculate WaveX frequencies when using n_freqs Usually to be set as the length of the timing baseline the model is being used for Returns ------- + indices : list Indices that have been assigned to new WaveX components """ @@ -1362,19 +1365,23 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): def _translate_wave_freqs(om, k): - """Use Wave model WAVEOM parameter to calculate a WaveX WXFREQ_ frequency parameter for wave number k + """ + Use Wave model WAVEOM parameter to calculate a WaveX WXFREQ_ frequency parameter for wave number k Parameters ---------- - om: float or astropy.quantity.Quantity + + om : float or astropy.quantity.Quantity Base frequency of Wave model solution - parameter WAVEOM If float is given default units of 1/d assigned - k: int + k : int wave number to use to calculate WaveX WXFREQ_ frequency parameter Returns ------- - WXFREQ_ quantity in units 1/d that can be used in WaveX model""" + + WXFREQ_ quantity in units 1/d that can be used in WaveX model + """ if isinstance(om, u.quantity.Quantity): om.to(u.d**-1) else: @@ -1383,19 +1390,23 @@ def _translate_wave_freqs(om, k): def _translate_wavex_freqs(wxfreq, k): - """Use WaveX model WXFREQ_ parameters and wave number k to calculate the Wave model WAVEOM frequency parameter. + """ + Use WaveX model WXFREQ_ parameters and wave number k to calculate the Wave model WAVEOM frequency parameter. Parameters ---------- - wxfreq: float or astropy.quantity.Quantity + + wxfreq : float or astropy.quantity.Quantity WaveX frequency from which the WAVEOM parameter will be calculated If float is given default units of 1/d assigned - k: int + k : int wave number to use to calculate Wave WAVEOM parameter Returns ------- - WAVEOM quantity in units 1/d that can be used in Wave model""" + + WAVEOM quantity in units 1/d that can be used in Wave model + """ if isinstance(wxfreq, u.quantity.Quantity): wxfreq.to(u.d**-1) else: @@ -1414,7 +1425,8 @@ def _translate_wavex_freqs(wxfreq, k): def translate_wave_to_wavex(model): - """Go from a Wave model to a WaveX model + """ + Go from a Wave model to a WaveX model WaveX frequencies get calculated based on the Wave model WAVEOM parameter and the number of WAVE parameters. WXFREQ_000k = [WAVEOM * (k+1)] / [2 * pi] @@ -1423,12 +1435,13 @@ def translate_wave_to_wavex(model): Paramters --------- - model: pint.models.timing_model.TimingModel + model : pint.models.timing_model.TimingModel TimingModel containing a Wave model to be converted to a WaveX model Returns ------- - New timing model with converted WaveX model included""" + New timing model with converted WaveX model included + """ from pint.models.wavex import WaveX new_model = deepcopy(model) @@ -1456,23 +1469,25 @@ def translate_wave_to_wavex(model): def get_wavex_freqs(model, index=None, quantity=False): - """Return the WaveX frequencies for a timing model. + """ + Return the WaveX frequencies for a timing model. If index is specified, returns the frequencies corresponding to the user-provided indices. If index isn't specified, returns all WaveX frequencies in timing model Parameters ---------- - model: pint.models.timing_model.TimingModel + model : pint.models.timing_model.TimingModel Timing model from which to return WaveX frequencies - index: : float, int, list, np.ndarray, None + index : float, int, list, np.ndarray, None Number or list/array of numbers corresponding to WaveX frequencies to return - quantity: bool + quantity : bool If set to True, returns a list of astropy.quanitity.Quantity rather than a list of prefixParameters Returns ------- - List of WXFREQ_ parameters""" + List of WXFREQ_ parameters + """ if index is None: freqs = model.components["WaveX"].get_prefix_mapping_component("WXFREQ_") if len(freqs) == 1: @@ -1500,23 +1515,25 @@ def get_wavex_freqs(model, index=None, quantity=False): def get_wavex_amps(model, index=None, quantity=False): - """Return the WaveX amplitudes for a timing model. + """ + Return the WaveX amplitudes for a timing model. If index is specified, returns the sine/cosine amplitudes corresponding to the user-provided indices. If index isn't specified, returns all WaveX sine/cosine amplitudes in timing model Parameters ---------- - model: pint.models.timing_model.TimingModel + model : pint.models.timing_model.TimingModel Timing model from which to return WaveX frequencies - index: : float, int, list, np.ndarray, None + index : float, int, list, np.ndarray, None Number or list/array of numbers corresponding to WaveX amplitudes to return - quantity: bool + quantity : bool If set to True, returns a list of tuples of astropy.quanitity.Quantity rather than a list of prefixParameters tuples Returns ------- - List of WXSIN_ and WXCOS_ parameters""" + List of WXSIN_ and WXCOS_ parameters + """ if index is None: indices = ( model.components["WaveX"].get_prefix_mapping_component("WXSIN_").keys() @@ -1562,19 +1579,21 @@ def get_wavex_amps(model, index=None, quantity=False): def translate_wavex_to_wave(model): - """Go from a WaveX timing model to a Wave timing model. + """ + Go from a WaveX timing model to a Wave timing model. WARNING: Not every WaveX model can be appropriately translated into a Wave model. This is dependent on the user's choice of frequencies in the WaveX model. In order for a WaveX model to be able to be converted into a Wave model, every WaveX frequency must produce the same value of WAVEOM in the calculation: WAVEOM = [2 * pi * WXFREQ_000k] / (k + 1) Paramters --------- - model: pint.models.timing_model.TimingModel + model : pint.models.timing_model.TimingModel TimingModel containing a WaveX model to be converted to a Wave model Returns ------- - New timing model with converted Wave model included""" + New timing model with converted Wave model included + """ from pint.models.wave import Wave new_model = deepcopy(model) From d8521913bc92c97ff6926c90a4bed43e58da7a7c Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Sat, 26 Aug 2023 18:14:54 -0500 Subject: [PATCH 101/144] put CHANGELOG back --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index d7206cbe3..8b63b52d7 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -12,4 +12,5 @@ the released changes. ### Added ### Fixed - Fixed RTD by specifying theme explicitly. +- Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` ### Removed From dbe69cc214427d2e015cd9b17b1d89d840c55ab1 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Sat, 26 Aug 2023 18:50:11 -0500 Subject: [PATCH 102/144] fixed .value for pairParameters --- CHANGELOG-unreleased.md | 1 + src/pint/models/parameter.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index d7206cbe3..380a8b3c4 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -12,4 +12,5 @@ the released changes. ### Added ### Fixed - Fixed RTD by specifying theme explicitly. +- `.value()` now works for pairParameters ### Removed diff --git a/src/pint/models/parameter.py b/src/pint/models/parameter.py index 8a14e0106..016687563 100644 --- a/src/pint/models/parameter.py +++ b/src/pint/models/parameter.py @@ -776,6 +776,9 @@ def _get_value(self, quan): return None elif isinstance(quan, (float, np.longdouble)): return quan + elif isinstance(quan, list): + # for pairParamters + return [x.to(self.units).value for x in quan] else: return quan.to(self.units).value From f349c876183667804608d80f29ed6e4cb6e29293 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 28 Aug 2023 15:13:34 -0500 Subject: [PATCH 103/144] Minor edits from PR comments --- src/pint/models/wave.py | 4 ++-- src/pint/models/wavex.py | 12 +++++++----- src/pint/utils.py | 4 ++-- tests/test_wavex.py | 5 ++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pint/models/wave.py b/src/pint/models/wave.py index 2092833a5..194b2b5b8 100644 --- a/src/pint/models/wave.py +++ b/src/pint/models/wave.py @@ -109,7 +109,7 @@ def add_wave_component(self, amps, index=None): ------- index : - Index that has been addigned to new Wave component + Index that has been assigned to new Wave component """ #### If index is None, increment the current max Wave index by 1. Increment using WAVE if index is None: @@ -138,7 +138,7 @@ def add_wave_component(self, amps, index=None): ) self.setup() self.validate() - return index + return f"{index}" def wave_phase(self, toas, delays): times = 0 diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index d45b9cc7e..d17762a91 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -22,21 +22,23 @@ class WaveX(DelayComponent): :class: pint.models.wavex.WaveX This is an extension of the L13 method described in Lentati et al., 2013 doi: 10.1103/PhysRevD.87.104021 - This model is similar to the TEMPO2 WAVE model parameters and users can convert a TimingModel with a Wave model + This model is similar to the TEMPO2 WAVE model parameters and users can convert a `TimingModel` with a Wave model to a WaveX model and produce the same results. The main differences are that the WaveX frequencies are explicitly stated, they do not necessarily need to be harmonics of some base frequency, the wave amplitudes are fittable parameters, and the - sine and cosine amplutides are reported as separate prefixParameters rather than as a single pairParameter. + sine and cosine amplutides are reported as separate `prefixParameter`s rather than as a single `pairParameter`. Analogous parameters in both models have the same units: WAVEEPOCH is the same as WXEPOCH WAVEOM and WXFREQ_000N have units of 1/d WAVEN and WXSIN_000N/WXCOS_000N have units of seconds - The pint.utils functions translate_wave_to_wavex() and translate_wavex_to_wave() can be used to go back and forth between - two model. WARNING: If the choice of WaveX frequencies in a TimingModel doesn't correspond to harmonics of some base + The `pint.utils` functions `translate_wave_to_wavex()` and `translate_wavex_to_wave()` can be used to go back and forth between + two model. + + WARNING: If the choice of WaveX frequencies in a `TimingModel` doesn't correspond to harmonics of some base freqeuncy, it will not be possible to convert it to a Wave model. - To set up a WaveX model, users can use the pint.utils function wavex_setup() with either a list of frequencies or a choice + To set up a WaveX model, users can use the `pint.utils` function `wavex_setup()` with either a list of frequencies or a choice of harmonics of a base frequency determined by 2 * pi /Timespan """ diff --git a/src/pint/utils.py b/src/pint/utils.py index a5a8c1e05..6ba3187f8 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1326,7 +1326,7 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): "Both freqs and n_freqs are specified. Only one or the other should be used" ) - if n_freqs == 0: + if n_freqs <= 0: raise ValueError("Must use a non-zero number of wave frequencies") model.add_component(WaveX()) if isinstance(T_span, u.quantity.Quantity): @@ -1446,7 +1446,7 @@ def translate_wave_to_wavex(model): new_model = deepcopy(model) wave_names = [ - "WAVE%d" % ii for ii in range(1, model.components["Wave"].num_wave_terms + 1) + f"WAVE{ii}" for ii in range(1, model.components["Wave"].num_wave_terms + 1) ] wave_terms = [getattr(model.components["Wave"], name) for name in wave_names] wave_om = model.components["Wave"].WAVE_OM.quantity diff --git a/tests/test_wavex.py b/tests/test_wavex.py index b066e6d7e..8ba91b10d 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -1,7 +1,7 @@ from io import StringIO import pytest import numpy as np -import logging +from loguru import logger as log from astropy import units as u from pint.models import get_model, get_model_and_toas @@ -88,7 +88,6 @@ def test_derivative(): model.WXSIN_0001.value = 0.01 model.WXCOS_0001.value = 0.05 toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") - log = logging.getLogger("TestWaveXDerivative") p = "WXSIN_0001" log.debug("Running derivative for %s", f"d_delay_d_{p}") ndf = model.d_delay_d_param_num(toas, p) @@ -106,7 +105,7 @@ def test_derivative(): log.debug( ( "derivative relative diff for %s, %lf" - % (f"d_delay_d_{p}", np.nanmax(relative_diff).value) + % (f"d_delay_d_{p}", f"{np.nanmax(relative_diff).value}") ) ) assert np.nanmax(relative_diff) < tol, msg From 6cd0cd99944a963284a6fa390277a9ef1a406d88 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 28 Aug 2023 15:28:46 -0500 Subject: [PATCH 104/144] Fixed changelog --- CHANGELOG-unreleased.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index d165b6c9f..1c0dfb9dc 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -10,10 +10,8 @@ the released changes. ## Unreleased ### Changed ### Added -- Third-order Roemer delay terms to ELL1 model - Added WaveX model as DelayComponent with wave amplitudes as fitted parameters -- Options to add a TZR TOA (`AbsPhase`) during the creation of a `TimingModel` using `ModelBuilder.__call__`, `get_model`, and `get_model_and_toas` -- `pint.print_info()` function for bug reporting ### Fixed - Fixed RTD by specifying theme explicitly. +- Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` ### Removed From 9e3cd5b20b9b5f982e276b7285d092d041914ebf Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Mon, 28 Aug 2023 15:42:48 -0500 Subject: [PATCH 105/144] Changed to fstrings --- tests/test_wavex.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 8ba91b10d..b40aeba30 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -89,7 +89,7 @@ def test_derivative(): model.WXCOS_0001.value = 0.05 toas = make_fake_toas_uniform(55000, 55100, 100, model, obs="gbt") p = "WXSIN_0001" - log.debug("Running derivative for %s", f"d_delay_d_{p}") + log.debug(f"Running derivative for {p}", f"d_delay_d_{p}") ndf = model.d_delay_d_param_num(toas, p) adf = model.d_delay_d_param(toas, p) diff = ndf - adf @@ -97,15 +97,11 @@ def test_derivative(): if np.all(diff.value) != 0.0: mean_der = (adf + ndf) / 2.0 relative_diff = np.abs(diff) / np.abs(mean_der) - msg = ( - "Derivative test failed at d_delay_d_%s with max relative difference %lf" - % (p, np.nanmax(relative_diff).value) - ) + msg = f"Derivative test failed at d_delay_d_{p} with max relative difference {np.nanmax(relative_diff).value}" tol = 0.7 log.debug( ( - "derivative relative diff for %s, %lf" - % (f"d_delay_d_{p}", f"{np.nanmax(relative_diff).value}") + f"derivative relative diff for d_delay_d_{p}, {np.nanmax(relative_diff).value}" ) ) assert np.nanmax(relative_diff) < tol, msg From a286fadd07b30d58494957d888b710caf4c00542 Mon Sep 17 00:00:00 2001 From: munahaf Date: Tue, 29 Aug 2023 06:24:04 +0000 Subject: [PATCH 106/144] Comment: Replaced a test expression to remove a logical short circuit. --- src/pint/pintk/plk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/pintk/plk.py b/src/pint/pintk/plk.py index 14831aefb..d5214f922 100644 --- a/src/pint/pintk/plk.py +++ b/src/pint/pintk/plk.py @@ -1508,7 +1508,7 @@ def canvasKeyEvent(self, event): self.updateJumped(jump_name) if ( self.selected is not None - and self.selected is not [] + and self.selected != [] and all(self.selected) ): self.psr.selected_toas = self.all_toas[self.selected] From 78d8db4e1253999b4acb9478fbb717911cee4d12 Mon Sep 17 00:00:00 2001 From: munahaf Date: Mon, 28 Aug 2023 20:01:38 +0000 Subject: [PATCH 107/144] Comment: Removed an undefined name. --- src/pint/models/tcb_conversion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pint/models/tcb_conversion.py b/src/pint/models/tcb_conversion.py index c4adccfed..48fb20d2b 100644 --- a/src/pint/models/tcb_conversion.py +++ b/src/pint/models/tcb_conversion.py @@ -9,7 +9,6 @@ "IFTE_K", "scale_parameter", "transform_mjd_parameter", - "convert_tcb_to_tdb", ] # These constants are taken from Irwin & Fukushima 1999. From 53d4862e3de61e21ab9f48b7cbef5be5461b62ef Mon Sep 17 00:00:00 2001 From: munahaf Date: Tue, 29 Aug 2023 06:36:03 +0000 Subject: [PATCH 108/144] Comment: Remove an undefined name. --- src/pint/fitter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 22e9860b3..d27511a1e 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -88,7 +88,6 @@ __all__ = [ "Fitter", - "auto", "WLSFitter", "GLSFitter", "WidebandTOAFitter", From 8c63186de8bc583f9b198ef37b7909f00926cb2d Mon Sep 17 00:00:00 2001 From: Munawar Date: Tue, 29 Aug 2023 07:24:07 -0700 Subject: [PATCH 109/144] Comment: Updated an exported name based on reviewer feedback. --- src/pint/models/tcb_conversion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pint/models/tcb_conversion.py b/src/pint/models/tcb_conversion.py index 48fb20d2b..13dd65fc6 100644 --- a/src/pint/models/tcb_conversion.py +++ b/src/pint/models/tcb_conversion.py @@ -9,6 +9,7 @@ "IFTE_K", "scale_parameter", "transform_mjd_parameter", + "convert_tcb_tdb", ] # These constants are taken from Irwin & Fukushima 1999. From 08e958eda43cb61632a32acceb5388e1cf639863 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 29 Aug 2023 10:06:02 -0500 Subject: [PATCH 110/144] update docs --- docs/examples/understanding_fitters.py | 8 +++++--- docs/explanation.rst | 18 ++++++++++++++---- src/pint/residuals.py | 9 ++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/examples/understanding_fitters.py b/docs/examples/understanding_fitters.py index 9908c7d55..e68832976 100644 --- a/docs/examples/understanding_fitters.py +++ b/docs/examples/understanding_fitters.py @@ -144,7 +144,7 @@ # straightforward but the full covariance matrix may be enormous. # If False, an algorithm is used that takes advantage of the structure # of the covariance matrix, based on information provided by the noise -# model. The two algorithms should give the same result to numerical +# model. The two algorithms should give the same result up to numerical # accuracy where they both can be applied. # %% [markdown] @@ -206,7 +206,8 @@ # %% [markdown] # ## Choosing fitters # -# You can use the automatic fitter selection to help you choose between `WLSFitter`, `GLSFitter`, and their wideband variants. The default `Downhill` fitters generally have better performance than the plain variants. +# You can use the automatic fitter selection to help you choose between `WLSFitter`, `GLSFitter`, and their wideband variants. +# The default `Downhill` fitters generally have better performance than the plain variants. # %% autofit = pint.fitter.Fitter.auto(toas=ts1855, model=m1855) @@ -221,4 +222,5 @@ # The results are (thankfully) identical. # %% [markdown] -# The MCMC fitter is considerably more complicated, so it has its own dedicated walkthroughs in `MCMC_walkthrough.ipynb` (for photon data) and `examples/fit_NGC6440E_MCMC.py` (for fitting TOAs). +# The MCMC fitter is considerably more complicated, so it has its own dedicated walkthroughs in `MCMC_walkthrough.ipynb` +# (for photon data) and `examples/fit_NGC6440E_MCMC.py` (for fitting TOAs). diff --git a/docs/explanation.rst b/docs/explanation.rst index f1ee98869..d7df0766e 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -716,13 +716,16 @@ model and data require different calculations - narrowband (TOA-only) versus wideband (TOA and DM measurements) and uncorrelated errors versus correlated errors. -The TEMPO/TEMPO2 and default PINT fitting algorithms (:class:`pint.fitter.WidebandTOAFitter` for example), leaving aside the rank-reduced case, proceed like: +The TEMPO/TEMPO2 and default PINT fitting algorithms (:class:`pint.fitter.WidebandTOAFitter`, for example), +leaving aside the rank-reduced case, proceed like: 1. Evaluate the model and its derivatives at the starting point :math:`x`, producing a set of residuals :math:`\delta y` and a Jacobian `M`. 2. Compute :math:`\delta x` to minimize :math:`\left| M\delta x - \delta y \right|_C`, where :math:`\left| \cdot \right|_C` is the squared amplitude of a vector with respect to the data uncertainties/covariance :math:`C`. 3. Update the starting point by :math:`\delta x`. -TEMPO and TEMPO2 can check whether the predicted improvement of chi-squared, assuming the linear model is correct, is enough to warrant continuing; if so, they jump back to step 1 unless the maximum number of iterations is reached. PINT does not contain this check. +TEMPO and TEMPO2 can check whether the predicted improvement of chi-squared, assuming +the linear model is correct, is enough to warrant continuing; if so, they jump back to +step 1 unless the maximum number of iterations is reached. PINT does not contain this check. This algorithm is the Gauss-Newton_algorithm_ for solving nonlinear least-squares problems, and even in one-complex-dimensional cases can exhibit @@ -744,9 +747,16 @@ PINT contains a slightly more sophisticated algorithm, implemented in 4. Evaluate the model at the starting point plus :math:`\lambda \delta x`. If this is invalid or worse than the starting point, divide :math:`\lambda` by two and repeat this step. If :math:`\lambda` is too small, accept the best point seen to date and exit without convergence. 5. If the model improved but only slightly with :math:`\lambda=1`, exit with convergence. If the maximum number of iterations was reached, exit without convergence. Otherwise update the starting point and return to step 1. -This ensures that PINT tries taking smaller steps if problems arise, and claims convergence only if a normal step worked. It does not solve the problems that arise if some parameters are nearly degenerate, enough to cause problems with the numerical linear algebra. +This ensures that PINT tries taking smaller steps if problems arise, and claims convergence +only if a normal step worked. It does not solve the problems that arise if some parameters are +nearly degenerate, enough to cause problems with the numerical linear algebra. -As a rule, this kind of problem is addressed with the Levenberg-Marquardt algorithm, which operates on the same principle of taking reduced steps when the derivative appears not to match the function, but does so in a way that also reduces issues with degenerate parameters; unfortunately it is not clear how to adapt this problem to the rank-reduced case. Nevertheless PINT contains an implementation, in :class:`pint.fitter.WidebandLMFitter`, but it does not perform as well as one might hope in practice and must be considered experimental. +As a rule, this kind of problem is addressed with the Levenberg-Marquardt algorithm, which +operates on the same principle of taking reduced steps when the derivative appears not to +match the function, but does so in a way that also reduces issues with degenerate parameters; +unfortunately it is not clear how to adapt this problem to the rank-reduced case. Nevertheless, +PINT contains an implementation in :class:`pint.fitter.WidebandLMFitter`, but it does not perform as +well as one might hope in practice and must be considered experimental. .. _Gauss-Newton_algorithm: https://en.wikipedia.org/wiki/Gauss%E2%80%93Newton_algorithm .. _chaotic: https://en.wikipedia.org/wiki/Newton_fractal diff --git a/src/pint/residuals.py b/src/pint/residuals.py index aec1645c6..736c5798b 100644 --- a/src/pint/residuals.py +++ b/src/pint/residuals.py @@ -501,6 +501,9 @@ def calc_time_resids( return (phase_resids / self.get_PSR_freq(calctype=calctype)).to(u.s) def _calc_gls_chi2(self): + """Compute the chi2 when correlated noise is present in the timing model. + If the system is not singular, it uses Cholesky factorization to evaluate this. + If the system is singular, it uses singular value decomposition instead.""" self.update() residuals = self.time_resids.to(u.s).value @@ -528,7 +531,7 @@ def _calc_gls_chi2(self): xhat = cho_solve(c, mtcy) except LinAlgError as e: log.warning( - f"Degenerate conditions encountered when " f"computing chi-squared: {e}" + f"Degenerate conditions encountered when computing chi-squared: {e}" ) U, s, Vt = scipy.linalg.svd(mtcm, full_matrices=False) @@ -550,8 +553,8 @@ def calc_chi2(self): If the errors on the TOAs are independent this is a straightforward calculation, but if the noise model introduces correlated errors then obtaining a meaningful chi-squared value requires a Cholesky - decomposition. This is carried out, here, by constructing a GLSFitter - and asking it to do the chi-squared computation but not a fit. + decomposition. This is carried out using the :method:`~pint.residuals.Residuals._calc_gls_chi2` + helper function. The return value here is available as self.chi2, which will not redo the computation unless necessary. From e4ded913a7429f8be46b913b54940609453eb507 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 29 Aug 2023 10:50:01 -0500 Subject: [PATCH 111/144] fixed ICRS -> ECL --- CHANGELOG-unreleased.md | 1 + src/pint/models/astrometry.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index e453c5a2c..f20722995 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -14,4 +14,5 @@ the released changes. - Fixed RTD by specifying theme explicitly. - `.value()` now works for pairParameters - Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` +- Fix ICRS -> ECL conversion when parameter uncertainties are not set. ### Removed diff --git a/src/pint/models/astrometry.py b/src/pint/models/astrometry.py index 38b5ac7c6..60e517c32 100644 --- a/src/pint/models/astrometry.py +++ b/src/pint/models/astrometry.py @@ -550,8 +550,12 @@ def as_ECL(self, epoch=None, ecl="IERS2010"): ra=self.RAJ.quantity, dec=self.DECJ.quantity, obstime=self.POSEPOCH.quantity, - pm_ra_cosdec=self.RAJ.uncertainty * np.cos(self.DECJ.quantity) / dt, - pm_dec=self.DECJ.uncertainty / dt, + pm_ra_cosdec=self.RAJ.uncertainty * np.cos(self.DECJ.quantity) / dt + if self.RAJ.uncertainty is not None + else 0 * self.RAJ.units / dt, + pm_dec=self.DECJ.uncertainty / dt + if self.DECJ.uncertainty is not None + else 0 * self.DECJ.units / dt, frame=coords.ICRS, ) c_ECL = c.transform_to(PulsarEcliptic(ecl=ecl)) @@ -563,8 +567,12 @@ def as_ECL(self, epoch=None, ecl="IERS2010"): ra=self.RAJ.quantity, dec=self.DECJ.quantity, obstime=self.POSEPOCH.quantity, - pm_ra_cosdec=self.PMRA.uncertainty, - pm_dec=self.PMDEC.uncertainty, + pm_ra_cosdec=self.PMRA.uncertainty + if self.PMRA.uncertainty is not None + else 0 * self.PMRA.units, + pm_dec=self.PMDEC.uncertainty + if self.PMDEC.uncertainty is not None + else 0 * self.PMDEC.units, frame=coords.ICRS, ) c_ECL = c.transform_to(PulsarEcliptic(ecl=ecl)) From 40fd78e763adab24b1bef198009e2b7ad1a1e0ae Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 29 Aug 2023 10:58:44 -0500 Subject: [PATCH 112/144] fix test --- tests/test_modelconversions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_modelconversions.py b/tests/test_modelconversions.py index dddae8d9c..99b328e63 100644 --- a/tests/test_modelconversions.py +++ b/tests/test_modelconversions.py @@ -73,10 +73,10 @@ def test_ECL_to_ICRS(): def test_ICRS_to_ECL_nouncertainties(): # start with ICRS model with no pm uncertainties, get residuals with ECL model, compare model_ICRS = get_model(io.StringIO(modelstring_ICRS)) - for p in ["PMRA", "PMDEC"]: - getattr(model_ICRS, p).frozen = True - getattr(model_ICRS, p).uncertainties = None - + model_ICRS.PMRA._uncertainty = None + model_ICRS.PMRA.frozen = True + model_ICRS.PMDEC._uncertainty = None + model_ICRS.PMDEC.frozen = True toas = pint.simulation.make_fake_toas_uniform( MJDStart, MJDStop, NTOA, model=model_ICRS, error=1 * u.us, add_noise=True ) @@ -89,10 +89,10 @@ def test_ICRS_to_ECL_nouncertainties(): def test_ECL_to_ICRS_nouncertainties(): # start with ECL model with no pm uncertainties, get residuals with ICRS model, compare model_ECL = get_model(io.StringIO(modelstring_ECL)) - for p in ["PMELONG", "PMELAT"]: - getattr(model_ECL, p).frozen = True - getattr(model_ECL, p).uncertainties = None - + model_ECL.PMELONG._uncertainty = None + model_ECL.PMELONG.frozen = True + model_ECL.PMELAT._uncertainty = None + model_ECL.PMELAT.frozen = True toas = pint.simulation.make_fake_toas_uniform( MJDStart, MJDStop, NTOA, model=model_ECL, error=1 * u.us, add_noise=True ) From 62de3909602ba171d74f52c19a706e759e93c934 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 29 Aug 2023 10:58:44 -0500 Subject: [PATCH 113/144] Added wave.py changes to changelog --- CHANGELOG-unreleased.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 847383d42..3308ec513 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -9,9 +9,11 @@ the released changes. ## Unreleased ### Changed +- WAVE parameters can be added to a Wave model with `add_wave_component()` in wave.py ### Added - Added WaveX model as DelayComponent with wave amplitudes as fitted parameters ### Fixed +- Wave model `validate()` can correctly use PEPOCH to assign WAVEEPOCH parameter - Fixed RTD by specifying theme explicitly. - `.value()` now works for pairParameters - Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` From a77b58611533e0dee06412a5195cf867ea02f0b2 Mon Sep 17 00:00:00 2001 From: Colin Clark Date: Tue, 18 Jul 2023 09:52:28 +0200 Subject: [PATCH 114/144] Correction to docstring for pbprime functions --- src/pint/models/stand_alone_psr_binaries/binary_orbits.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pint/models/stand_alone_psr_binaries/binary_orbits.py b/src/pint/models/stand_alone_psr_binaries/binary_orbits.py index 7ee4f6b81..3a1d60bbb 100644 --- a/src/pint/models/stand_alone_psr_binaries/binary_orbits.py +++ b/src/pint/models/stand_alone_psr_binaries/binary_orbits.py @@ -29,7 +29,7 @@ def orbit_phase(self): return (orbits - norbits) * 2 * np.pi * u.rad def pbprime(self): - """Derivative of binary period with respect to time.""" + """Instantaneous binary period as a function of time.""" raise NotImplementedError def pbdot_orbit(self): @@ -105,7 +105,7 @@ def orbits(self): ).decompose() def pbprime(self): - """Derivative of binary period with respect to time.""" + """Instantaneous binary period as a function of time.""" return self.PB + self.PBDOT * self.tt0 def pbdot_orbit(self): @@ -186,7 +186,7 @@ def orbits(self): return orbits.decompose() def pbprime(self): - """Derivative of binary period with respect to time.""" + """Instantaneous binary period as a function of time.""" orbit_freq = taylor_horner_deriv(self.tt0, self._FBXs(), 1) return 1.0 / orbit_freq From 5e701fa582d87bd8a0a4e7579566b69d039fe5fa Mon Sep 17 00:00:00 2001 From: Colin Clark Date: Tue, 29 Aug 2023 20:45:23 +0200 Subject: [PATCH 115/144] Noted the pbprime docstring change in change log --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index e453c5a2c..edb8717d4 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -14,4 +14,5 @@ the released changes. - Fixed RTD by specifying theme explicitly. - `.value()` now works for pairParameters - Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` +- Fixed an incorrect docstring in pbprime() functions. ### Removed From 1586dacb98f3776febf629f70ca191f8b1b9afe6 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 29 Aug 2023 13:55:49 -0500 Subject: [PATCH 116/144] Added more tests and edited documentation --- src/pint/models/wavex.py | 14 +++++++------- tests/test_wavex.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index d17762a91..2861308c1 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -50,7 +50,7 @@ def __init__(self): self.add_param( MJDParameter( name="WXEPOCH", - description="Reference epoch for wave delay solution", + description="Reference epoch for Fourier representation of red noise", time_scale="tdb", ) ) @@ -103,7 +103,7 @@ def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True) self.add_param( prefixParameter( name=f"WXFREQ_{i}", - description="Base frequency of wave delay solution", + description="Component frequency for Fourier representation of red noise", units="1/d", value=wxfreq, parameter_type="float", @@ -112,7 +112,7 @@ def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True) self.add_param( prefixParameter( name=f"WXSIN_{i}", - description="Sine amplitudes for wave delay function", + description="Sine amplitudes for Fourier representation of red noise", units="s", value=wxsin, frozen=frozen, @@ -122,7 +122,7 @@ def add_wavex_component(self, wxfreq, index=None, wxsin=0, wxcos=0, frozen=True) self.add_param( prefixParameter( name=f"WXCOS_{i}", - description="Cosine amplitudes for wave delay function", + description="Cosine amplitudes for Fourier representation of red noise", units="s", value=wxcos, frozen=frozen, @@ -214,7 +214,7 @@ def add_wavex_components( self.add_param( prefixParameter( name=f"WXFREQ_{i}", - description="Base frequency of wave delay solution", + description="Component frequency for Fourier representation of red noise", units="1/d", value=wxfreq, parameter_type="float", @@ -223,7 +223,7 @@ def add_wavex_components( self.add_param( prefixParameter( name=f"WXSIN_{i}", - description="Sine amplitudes for wave delay function", + description="Sine amplitude for Fourier representation of red noise", units="s", value=wxsin, parameter_type="float", @@ -233,7 +233,7 @@ def add_wavex_components( self.add_param( prefixParameter( name=f"WXCOS_{i}", - description="Cosine amplitudes for wave delay function", + description="Cosine amplitude for Fourier representation of red noise", units="s", value=wxcos, parameter_type="float", diff --git a/tests/test_wavex.py b/tests/test_wavex.py index b40aeba30..76d01c7e7 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -190,6 +190,29 @@ def test_add_wavex_to_par(): ) +def test_add_existing_index(): + # Check that trying to add an existing index fails + model = get_model(StringIO(par2 + wavex_par)) + with pytest.raises(ValueError): + index = model.components["WaveX"].add_wavex_component(0.01, index=2) + + +def test_add_existing_indices(): + # Check that trying to add multiple existing indices fails + model = get_model(StringIO(par2 + wavex_par)) + with pytest.raises(ValueError): + indices = model.components["WaveX"].add_wavex_components( + [0.01, 0.02], indices=[2, 3] + ) + + +def test_multiple_wavex_none_indices(): + model = get_model(StringIO(par2 + wavex_par)) + model.components["WaveX"].add_wavex_components([0.01, 0.02]) + indices = model.components["WaveX"].get_indices() + assert np.all(indices == np.array(range(1, len(indices) + 1))) + + def test_add_then_remove_wavex(): # Check that adding and then removing a wavex component actually gets rid of it model = get_model(StringIO(par2)) @@ -215,6 +238,10 @@ def test_multiple_wavex(): ) +# def test_multiple_wavex_unit_conversion(): +# Check that input frequencies and amplitudes in different units convert properly + + def test_multiple_wavex_broadcast_frozens(): # Check that when a single False is given for frozens, it gets broadcast to all the sine and cosine amplitudes model = get_model(StringIO(par2)) From 058eab610792829d1730f3f8e9812e92c7f6a01b Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 29 Aug 2023 14:13:12 -0500 Subject: [PATCH 117/144] added parentheses and fixed other conversion --- src/pint/models/astrometry.py | 60 +++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/pint/models/astrometry.py b/src/pint/models/astrometry.py index 60e517c32..1d737849c 100644 --- a/src/pint/models/astrometry.py +++ b/src/pint/models/astrometry.py @@ -550,12 +550,16 @@ def as_ECL(self, epoch=None, ecl="IERS2010"): ra=self.RAJ.quantity, dec=self.DECJ.quantity, obstime=self.POSEPOCH.quantity, - pm_ra_cosdec=self.RAJ.uncertainty * np.cos(self.DECJ.quantity) / dt - if self.RAJ.uncertainty is not None - else 0 * self.RAJ.units / dt, - pm_dec=self.DECJ.uncertainty / dt - if self.DECJ.uncertainty is not None - else 0 * self.DECJ.units / dt, + pm_ra_cosdec=( + self.RAJ.uncertainty * np.cos(self.DECJ.quantity) / dt + if self.RAJ.uncertainty is not None + else 0 * self.RAJ.units / dt + ), + pm_dec=( + self.DECJ.uncertainty / dt + if self.DECJ.uncertainty is not None + else 0 * self.DECJ.units / dt + ), frame=coords.ICRS, ) c_ECL = c.transform_to(PulsarEcliptic(ecl=ecl)) @@ -567,12 +571,16 @@ def as_ECL(self, epoch=None, ecl="IERS2010"): ra=self.RAJ.quantity, dec=self.DECJ.quantity, obstime=self.POSEPOCH.quantity, - pm_ra_cosdec=self.PMRA.uncertainty - if self.PMRA.uncertainty is not None - else 0 * self.PMRA.units, - pm_dec=self.PMDEC.uncertainty - if self.PMDEC.uncertainty is not None - else 0 * self.PMDEC.units, + pm_ra_cosdec=( + self.PMRA.uncertainty + if self.PMRA.uncertainty is not None + else 0 * self.PMRA.units + ), + pm_dec=( + self.PMDEC.uncertainty + if self.PMDEC.uncertainty is not None + else 0 * self.PMDEC.units + ), frame=coords.ICRS, ) c_ECL = c.transform_to(PulsarEcliptic(ecl=ecl)) @@ -951,8 +959,16 @@ def as_ECL(self, epoch=None, ecl="IERS2010"): lat=self.ELAT.quantity, obliquity=OBL[self.ECL.value], obstime=self.POSEPOCH.quantity, - pm_lon_coslat=self.ELONG.uncertainty * np.cos(self.ELAT.quantity) / dt, - pm_lat=self.ELAT.uncertainty / dt, + pm_lon_coslat=( + self.ELONG.uncertainty * np.cos(self.ELAT.quantity) / dt + if self.ELONG.uncertainty is not None + else 0 * self.ELONG.units / dt + ), + pm_lat=( + self.ELAT.uncertainty / dt + if self.ELAT.uncertainty is not None + else 0 * self.ELAT.units / dt + ), frame=PulsarEcliptic, ) c_ECL = c.transform_to(PulsarEcliptic(ecl=ecl)) @@ -965,12 +981,16 @@ def as_ECL(self, epoch=None, ecl="IERS2010"): lat=self.ELAT.quantity, obliquity=OBL[self.ECL.value], obstime=self.POSEPOCH.quantity, - pm_lon_coslat=self.PMELONG.uncertainty - if self.PMELONG.uncertainty is not None - else 0 * self.PMELONG.units, - pm_lat=self.PMELAT.uncertainty - if self.PMELAT.uncertainty is not None - else 0 * self.PMELAT.units, + pm_lon_coslat=( + self.PMELONG.uncertainty + if self.PMELONG.uncertainty is not None + else 0 * self.PMELONG.units + ), + pm_lat=( + self.PMELAT.uncertainty + if self.PMELAT.uncertainty is not None + else 0 * self.PMELAT.units + ), frame=PulsarEcliptic, ) c_ECL = c.transform_to(PulsarEcliptic(ecl=ecl)) From e1b0ee72225df38373aab08b01bfced5ee77b26c Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Tue, 29 Aug 2023 14:47:13 -0500 Subject: [PATCH 118/144] Added unit conversion test --- tests/test_wavex.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 76d01c7e7..309c89ca4 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -238,8 +238,19 @@ def test_multiple_wavex(): ) -# def test_multiple_wavex_unit_conversion(): -# Check that input frequencies and amplitudes in different units convert properly +def test_multiple_wavex_unit_conversion(): + # Check that input frequencies and amplitudes in different units convert properly + model = get_model(StringIO(par2)) + freqs = [2e-7 * u.s**-1, 3e-7 * u.s**-1] + indices = model.components["WaveX"].add_wavex_components( + [2e-7 * u.s**-1, 3e-7 * u.s**-1], + indices=[2, 3], + wxsins=[2, 3], + wxcoses=[2, 3], + frozens=False, + ) + assert getattr(model, f"WXFREQ_0002").value == freqs[0].to(u.d**-1).value + assert getattr(model, f"WXFREQ_0003").value == freqs[1].to(u.d**-1).value def test_multiple_wavex_broadcast_frozens(): From 34e1b6e98240a59a11ef9fb7405ab8842901a66d Mon Sep 17 00:00:00 2001 From: Munawar Date: Tue, 29 Aug 2023 14:43:36 -0700 Subject: [PATCH 119/144] Comment: Updated based on black output. --- src/pint/pintk/plk.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pint/pintk/plk.py b/src/pint/pintk/plk.py index d5214f922..368e19599 100644 --- a/src/pint/pintk/plk.py +++ b/src/pint/pintk/plk.py @@ -1506,11 +1506,7 @@ def canvasKeyEvent(self, event): self.psr.selected_toas = self.psr.all_toas[cluster_bool] jump_name = self.psr.add_jump(cluster_bool) self.updateJumped(jump_name) - if ( - self.selected is not None - and self.selected != [] - and all(self.selected) - ): + if self.selected is not None and self.selected != [] and all(self.selected): self.psr.selected_toas = self.all_toas[self.selected] self.fitboxesWidget.addFitCheckBoxes(self.psr.prefit_model) self.randomboxWidget.addRandomCheckbox(self) From 72d817ff22f0ce918547058dea020d5d689d013e Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Wed, 30 Aug 2023 11:17:55 -0500 Subject: [PATCH 120/144] cleanup --- docs/examples/understanding_fitters.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/examples/understanding_fitters.py b/docs/examples/understanding_fitters.py index e68832976..8647386a3 100644 --- a/docs/examples/understanding_fitters.py +++ b/docs/examples/understanding_fitters.py @@ -178,11 +178,6 @@ # %% glsfit.fit_toas(maxiter=1) -# %% -# Not sure how to do this properly yet. -# glsfit2 = pint.fitter.GLSFitter(toas=t, model=glsfit.model, residuals=glsfit.resids) -# glsfit2.fit_toas(maxiter=0) - # %% glsfit.print_summary() From 6b286367a3da0e6cfeb3213d881431fd6dd35111 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 10:47:19 -0500 Subject: [PATCH 121/144] is_wideband --- src/pint/toa.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pint/toa.py b/src/pint/toa.py index 014403918..cc5742878 100644 --- a/src/pint/toa.py +++ b/src/pint/toa.py @@ -1631,7 +1631,14 @@ def is_wideband(self): # there may be a more elegant way to do this dm_data, valid_data = self.get_flag_value("pp_dm", as_type=float) - return valid_data != [] + + if len(valid_data) not in [0, len(self)]: + log.warning( + "Only a subset of the TOAs have wideband DM information." + "A TOAs object will be treated wideband only of ALL TOAs have DM information." + ) + + return len(valid_data) == len(self) def get_all_flags(self): """Return a list of all the flags used by any TOA.""" From fd72267d057bdb061d9c0b5b2a239d9c7cf95710 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 10:54:44 -0500 Subject: [PATCH 122/144] test_is_wideband --- src/pint/toa.py | 4 ++-- tests/test_toa.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pint/toa.py b/src/pint/toa.py index cc5742878..210a167d2 100644 --- a/src/pint/toa.py +++ b/src/pint/toa.py @@ -1634,8 +1634,8 @@ def is_wideband(self): if len(valid_data) not in [0, len(self)]: log.warning( - "Only a subset of the TOAs have wideband DM information." - "A TOAs object will be treated wideband only of ALL TOAs have DM information." + "Only a subset of the TOAs have wideband DM information. " + "A TOAs object will be treated as wideband only of *ALL* TOAs have DM information." ) return len(valid_data) == len(self) diff --git a/tests/test_toa.py b/tests/test_toa.py index 20303f174..b262e198b 100644 --- a/tests/test_toa.py +++ b/tests/test_toa.py @@ -182,3 +182,17 @@ def test_merge_toas(): toas_out = pint.toa.merge_TOAs([toas, toas2]) toas_outb = toas + toas2 assert np.all(toas_out.table == toas_outb.table) + + +def test_is_wideband(): + t1 = pint.toa.get_TOAs( + io.StringIO( + """ + fake.ff 1430.000000 53393.561383615118386 0.178 ao -fe L-wide -be ASP -pp_dm 10.0 + fake.ff 1430.000000 53394.561383615118386 0.178 ao -fe L-wide -be ASP + """ + ) + ) + assert not t1.is_wideband() + assert t1[0].is_wideband() + assert not t1[1].is_wideband() From 4e12d0c612c6071bd4094b4d3327476dc966bdf0 Mon Sep 17 00:00:00 2001 From: LuluAgazie Date: Thu, 31 Aug 2023 10:59:03 -0500 Subject: [PATCH 123/144] More tests --- tests/test_wavex.py | 64 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/test_wavex.py b/tests/test_wavex.py index 309c89ca4..0c7ec4dca 100644 --- a/tests/test_wavex.py +++ b/tests/test_wavex.py @@ -6,7 +6,7 @@ from astropy import units as u from pint.models import get_model, get_model_and_toas from pint.models import model_builder as mb -from pint.models.timing_model import Component +from pint.models.timing_model import Component, MissingParameter from pint.fitter import Fitter from pint.residuals import Residuals from pint.toa import get_TOAs @@ -253,6 +253,68 @@ def test_multiple_wavex_unit_conversion(): assert getattr(model, f"WXFREQ_0003").value == freqs[1].to(u.d**-1).value +def test_cos_amp_missing(): + # Check that validate fails when using a model with missing cosine amplitudes for the frequencies present + bad_wavex_par = """ + WXFREQ_0002 0.2 + WXSIN_0002 2 + """ + with pytest.raises(ValueError): + model = get_model(StringIO(par2 + bad_wavex_par)) + + +def test_sin_amp_missing(): + # Check that validate fails when using a model with missing cosine amplitudes for the frequencies present + bad_wavex_par = """ + WXFREQ_0002 0.2 + WXCOS_0002 2 + """ + with pytest.raises(ValueError): + model = get_model(StringIO(par2 + bad_wavex_par)) + + +def test_bad_wxfreq_value(): + # Check that putting a zero, or None value for an added frequency raises ValueErrors + model = get_model(StringIO(par2)) + with pytest.raises(ValueError): + model.components["WaveX"].add_wavex_component(0) + model.components["WaveX"].add_wavex_component(None) + + +def test_missing_epoch_parameters(): + bad_par = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + DM 71.016633 + WXFREQ_0001 0.1 + WXSIN_0001 1 + WXCOS_0001 1 + UNITS TDB + """ + with pytest.raises(MissingParameter): + model = get_model(StringIO(bad_par)) + + +def test_sin_cos_mismatch(): + # Check that having mismatching sine and cosine amplitudes raises ValueErrors + bad_wavex_par = """ + WXFREQ_0002 0.2 + WXSIN_0002 2 + WXCOS_0003 2 + WXFREQ_0003 0.3 + WXSIN_0003 2 + WXCOS_0004 2 + """ + with pytest.raises(ValueError): + model = get_model(StringIO(par2 + bad_wavex_par)) + + def test_multiple_wavex_broadcast_frozens(): # Check that when a single False is given for frozens, it gets broadcast to all the sine and cosine amplitudes model = get_model(StringIO(par2)) From 74e63bf9f1a547ce405a4ec218e58c5d6b5382da Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 11:04:48 -0500 Subject: [PATCH 124/144] changelog --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index ac9bc2495..e998a4d5b 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -11,6 +11,7 @@ the released changes. ### Changed - Moved design matrix normalization code from `pint.fitter` to the new `pint.utils.normalize_designmatrix()` function. - Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). +- `TOAs.is_wideband` returns True only if *ALL* TOAs have the -pp_dm flag. Emits a warning if only some TOAs have this flag. ### Added ### Fixed - Fixed RTD by specifying theme explicitly. From 0fff91943458920a67388202e3103e088a473043 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 12:25:43 -0500 Subject: [PATCH 125/144] raise exception on read --- src/pint/toa.py | 16 +++++++++------- tests/test_toa.py | 20 +++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pint/toa.py b/src/pint/toa.py index 210a167d2..682ca60ce 100644 --- a/src/pint/toa.py +++ b/src/pint/toa.py @@ -321,6 +321,14 @@ def get_TOAs( if "pulse_number" in t.table.colnames and not include_pn: log.warning("'pulse_number' column exists but not being read in") t.remove_pulse_numbers() + + dm_data, valid_data = t.get_flag_value("pp_dm", as_type=float) + if len(valid_data) not in [0, len(t)]: + raise ValueError( + "Mixing narrowband and wideband toas in a TOAs object is not allowed. " + "Make sure that either all or no TOAs have the -pp_dm flag." + ) + return t @@ -1631,13 +1639,6 @@ def is_wideband(self): # there may be a more elegant way to do this dm_data, valid_data = self.get_flag_value("pp_dm", as_type=float) - - if len(valid_data) not in [0, len(self)]: - log.warning( - "Only a subset of the TOAs have wideband DM information. " - "A TOAs object will be treated as wideband only of *ALL* TOAs have DM information." - ) - return len(valid_data) == len(self) def get_all_flags(self): @@ -2993,4 +2994,5 @@ def get_TOAs_array( t.compute_TDBs(method=tdb_method, ephem=ephem) if "ssb_obs_pos" not in t.table.colnames: t.compute_posvels(ephem, planets) + return t diff --git a/tests/test_toa.py b/tests/test_toa.py index b262e198b..7b367ba6d 100644 --- a/tests/test_toa.py +++ b/tests/test_toa.py @@ -184,15 +184,13 @@ def test_merge_toas(): assert np.all(toas_out.table == toas_outb.table) -def test_is_wideband(): - t1 = pint.toa.get_TOAs( - io.StringIO( - """ - fake.ff 1430.000000 53393.561383615118386 0.178 ao -fe L-wide -be ASP -pp_dm 10.0 - fake.ff 1430.000000 53394.561383615118386 0.178 ao -fe L-wide -be ASP - """ +def test_mix_nb_wb(): + with pytest.raises(ValueError): + t1 = pint.toa.get_TOAs( + io.StringIO( + """ + fake.ff 1430.000000 53393.561383615118386 0.178 ao -fe L-wide -be ASP -pp_dm 10.0 + fake.ff 1430.000000 53394.561383615118386 0.178 ao -fe L-wide -be ASP + """ + ) ) - ) - assert not t1.is_wideband() - assert t1[0].is_wideband() - assert not t1[1].is_wideband() From d32e9954b30b260d742fffa28b1729e46adadd8c Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 12:27:53 -0500 Subject: [PATCH 126/144] CHANGELOG --- CHANGELOG-unreleased.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index e998a4d5b..9c0ab243d 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -11,7 +11,6 @@ the released changes. ### Changed - Moved design matrix normalization code from `pint.fitter` to the new `pint.utils.normalize_designmatrix()` function. - Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). -- `TOAs.is_wideband` returns True only if *ALL* TOAs have the -pp_dm flag. Emits a warning if only some TOAs have this flag. ### Added ### Fixed - Fixed RTD by specifying theme explicitly. @@ -19,4 +18,5 @@ the released changes. - Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` - Fixed an incorrect docstring in pbprime() functions. - Fix ICRS -> ECL conversion when parameter uncertainties are not set. +- `get_TOAs` raises an exception upon finding mixed narrowband and wideband TOAs in a tim file. `TOAs.is_wideband` returns True only if *ALL* TOAs have the -pp_dm flag. ### Removed From 4c6544af2361828edf52f8581e291c8eaebaacf6 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 14:07:10 -0500 Subject: [PATCH 127/144] dmwavex --- src/pint/models/__init__.py | 1 + src/pint/models/dmwavex.py | 378 ++++++++++++++++++++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 src/pint/models/dmwavex.py diff --git a/src/pint/models/__init__.py b/src/pint/models/__init__.py index 5440cca96..f5a46d3b9 100644 --- a/src/pint/models/__init__.py +++ b/src/pint/models/__init__.py @@ -25,6 +25,7 @@ from pint.models.binary_ddk import BinaryDDK from pint.models.binary_ell1 import BinaryELL1, BinaryELL1H, BinaryELL1k from pint.models.dispersion_model import DispersionDM, DispersionDMX +from pint.models.dmwavex import DMWaveX from pint.models.frequency_dependent import FD from pint.models.glitch import Glitch from pint.models.phase_offset import PhaseOffset diff --git a/src/pint/models/dmwavex.py b/src/pint/models/dmwavex.py new file mode 100644 index 000000000..9af2f95b4 --- /dev/null +++ b/src/pint/models/dmwavex.py @@ -0,0 +1,378 @@ +"""DM variations expressed as a sum of sinusoids.""" +import astropy.units as u +import numpy as np +from loguru import logger as log +from warnings import warn + +from pint.models.parameter import MJDParameter, prefixParameter +from pint.models.timing_model import MissingParameter +from pint.models.dispersion_model import Dispersion +from pint import dmu + + +class DMWaveX(Dispersion): + """ + Fourier representation of DM variations. + + Used for decomposition of DM noise into a series of sine/cosine components with the amplitudes as fitted parameters. + + Parameters supported: + + .. paramtable:: + :class: pint.models.dmwavex.DMWaveX + + To set up a DMWaveX model, users can use the `pint.utils` function `dmwavex_setup()` with either a list of frequencies or a choice + of harmonics of a base frequency determined by 2 * pi /Timespan + """ + + register = True + category = "dmwavex" + + def __init__(self): + super().__init__() + self.add_param( + MJDParameter( + name="DMWXEPOCH", + description="Reference epoch for Fourier representation of DM noise", + time_scale="tdb", + ) + ) + self.add_dmwavex_component(0.1, index=1, dmwxsin=0, dmwxcos=0, frozen=False) + self.set_special_params(["DMWXFREQ_0001", "DMWXSIN_0001", "DMWXCOS_0001"]) + self.dm_value_funcs += [self.dmwavex_dm] + self.delay_funcs_component += [self.dmwavex_delay] + + def add_dmwavex_component( + self, dmwxfreq, index=None, dmwxsin=0, dmwxcos=0, frozen=True + ): + """ + Add DMWaveX component + + Parameters + ---------- + + dmwxfreq : float or astropy.quantity.Quantity + Base frequency for DMWaveX component + index : int, None + Interger label for DMWaveX component. If None, will increment largest used index by 1. + dmwxsin : float or astropy.quantity.Quantity + Sine amplitude for DMWaveX component + dmwxcos : float or astropy.quantity.Quantity + Cosine amplitude for DMWaveX component + frozen : iterable of bool or bool + Indicates whether DMWaveX parameters will be fit + + Returns + ------- + + index : int + Index that has been assigned to new DMWaveX component + """ + + #### If index is None, increment the current max DMWaveX index by 1. Increment using DMWXFREQ + if index is None: + dct = self.get_prefix_mapping_component("DMWXFREQ_") + index = np.max(list(dct.keys())) + 1 + i = f"{int(index):04d}" + + if int(index) in self.get_prefix_mapping_component("DMWXFREQ_"): + raise ValueError( + f"Index '{index}' is already in use in this model. Please choose another" + ) + + if isinstance(dmwxsin, u.quantity.Quantity): + dmwxsin = dmwxsin.to_value(u.s) + if isinstance(dmwxcos, u.quantity.Quantity): + dmwxcos = dmwxcos.to_value(u.s) + if isinstance(dmwxfreq, u.quantity.Quantity): + dmwxfreq = dmwxfreq.to_value(1 / u.d) + self.add_param( + prefixParameter( + name=f"DMWXFREQ_{i}", + description="Component frequency for Fourier representation of DM noise", + units="1/d", + value=dmwxfreq, + parameter_type="float", + ) + ) + self.add_param( + prefixParameter( + name=f"DMWXSIN_{i}", + description="Sine amplitudes for Fourier representation of DM noise", + units=dmu, + value=dmwxsin, + frozen=frozen, + parameter_type="float", + ) + ) + self.add_param( + prefixParameter( + name=f"DMWXCOS_{i}", + description="Cosine amplitudes for Fourier representation of DM noise", + units=dmu, + value=dmwxcos, + frozen=frozen, + parameter_type="float", + ) + ) + self.setup() + self.validate() + return index + + def add_dmwavex_components( + self, dmwxfreqs, indices=None, dmwxsins=0, dmwxcoses=0, frozens=True + ): + """ + Add DMWaveX components with specified base frequencies + + Parameters + ---------- + + dmwxfreqs : iterable of float or astropy.quantity.Quantity + Base frequencies for DMWaveX components + indices : iterable of int, None + Interger labels for DMWaveX components. If None, will increment largest used index by 1. + dmwxsins : iterable of float or astropy.quantity.Quantity + Sine amplitudes for DMWaveX components + dmwxcoses : iterable of float or astropy.quantity.Quantity + Cosine amplitudes for DMWaveX components + frozens : iterable of bool or bool + Indicates whether sine adn cosine amplitudes of DMwavex components will be fit + + Returns + ------- + + indices : list + Indices that have been assigned to new DMWaveX components + """ + + if indices is None: + indices = [None] * len(dmwxfreqs) + dmwxsins = np.atleast_1d(dmwxsins) + dmwxcoses = np.atleast_1d(dmwxcoses) + if len(dmwxsins) == 1: + dmwxsins = np.repeat(dmwxsins, len(dmwxfreqs)) + if len(dmwxcoses) == 1: + dmwxcoses = np.repeat(dmwxcoses, len(dmwxfreqs)) + if len(dmwxsins) != len(dmwxfreqs): + raise ValueError( + f"Number of base frequencies {len(dmwxfreqs)} doesn't match number of sine ampltudes {len(dmwxsins)}" + ) + if len(dmwxcoses) != len(dmwxfreqs): + raise ValueError( + f"Number of base frequencies {len(dmwxfreqs)} doesn't match number of cosine ampltudes {len(dmwxcoses)}" + ) + frozens = np.atleast_1d(frozens) + if len(frozens) == 1: + frozens = np.repeat(frozens, len(dmwxfreqs)) + if len(frozens) != len(dmwxfreqs): + raise ValueError( + f"Number of base frequencies must match number of frozen values" + ) + #### If indices is None, increment the current max DMWaveX index by 1. Increment using DMWXFREQ + dct = self.get_prefix_mapping_component("DMWXFREQ_") + last_index = np.max(list(dct.keys())) + added_indices = [] + for dmwxfreq, index, dmwxsin, dmwxcos, frozen in zip( + dmwxfreqs, indices, dmwxsins, dmwxcoses, frozens + ): + if index is None: + index = last_index + 1 + last_index += 1 + elif index in list(dct.keys()): + raise ValueError( + f"Attempting to insert DMWXFREQ_{index:04d} but it already exists" + ) + added_indices.append(index) + i = f"{int(index):04d}" + + if int(index) in dct: + raise ValueError( + f"Index '{index}' is already in use in this model. Please choose another" + ) + if isinstance(dmwxfreq, u.quantity.Quantity): + dmwxfreq = dmwxfreq.to_value(u.d**-1) + if isinstance(dmwxsin, u.quantity.Quantity): + dmwxsin = dmwxsin.to_value(u.s) + if isinstance(dmwxcos, u.quantity.Quantity): + dmwxcos = dmwxcos.to_value(u.s) + log.trace(f"Adding DMWXSIN_{i} and DMWXCOS_{i} at frequency DMWXFREQ_{i}") + self.add_param( + prefixParameter( + name=f"DMWXFREQ_{i}", + description="Component frequency for Fourier representation of DM noise", + units="1/d", + value=dmwxfreq, + parameter_type="float", + ) + ) + self.add_param( + prefixParameter( + name=f"DMWXSIN_{i}", + description="Sine amplitude for Fourier representation of DM noise", + units=dmu, + value=dmwxsin, + parameter_type="float", + frozen=frozen, + ) + ) + self.add_param( + prefixParameter( + name=f"DMWXCOS_{i}", + description="Cosine amplitude for Fourier representation of DM noise", + units=dmu, + value=dmwxcos, + parameter_type="float", + frozen=frozen, + ) + ) + self.setup() + self.validate() + return added_indices + + def remove_dmwavex_component(self, index): + """ + Remove all DMWaveX components associated with a given index or list of indices + + Parameters + ---------- + index : float, int, list, np.ndarray + Number or list/array of numbers corresponding to DMWaveX indices to be removed from model. + """ + + if isinstance(index, (int, float, np.int64)): + indices = [index] + elif isinstance(index, (list, set, np.ndarray)): + indices = index + else: + raise TypeError( + f"index most be a float, int, set, list, or array - not {type(index)}" + ) + for index in indices: + index_rf = f"{int(index):04d}" + for prefix in ["DMWXFREQ_", "DMWXSIN_", "DMWXCOS_"]: + self.remove_param(prefix + index_rf) + self.validate() + + def get_indices(self): + """ + Returns an array of intergers corresponding to DMWaveX component parameters using DMWXFREQs + + Returns + ------- + inds : np.ndarray + Array of DMWaveX indices in model. + """ + inds = [int(p.split("_")[-1]) for p in self.params if "WXFREQ_" in p] + return np.array(inds) + + # Initialize setup + def setup(self): + super().setup() + # Get DMWaveX mapping and register DMWXSIN and DMWXCOS derivatives + for prefix_par in self.get_params_of_type("prefixParameter"): + if prefix_par.startswith("DMWXSIN_"): + self.register_deriv_funcs(self.d_dmwavex_delay_d_DMWXSIN, prefix_par) + if prefix_par.startswith("DMWXCOS_"): + self.register_deriv_funcs(self.d_dmwavex_delay_d_DMWXCOS, prefix_par) + self.wave_freqs = list( + self.get_prefix_mapping_component("DMWXFREQ_").keys() + ) + self.num_wave_freqs = len(self.wave_freqs) + + def validate(self): + # Validate all the DMWaveX parameters + super().validate() + self.setup() + DMWXFREQ_mapping = self.get_prefix_mapping_component("DMWXFREQ_") + DMWXSIN_mapping = self.get_prefix_mapping_component("DMWXSIN_") + DMWXCOS_mapping = self.get_prefix_mapping_component("DMWXCOS_") + if DMWXFREQ_mapping.keys() != DMWXSIN_mapping.keys(): + raise ValueError( + "WXFREQ_ parameters do not match DMWXSIN_ parameters." + "Please check your prefixed parameters" + ) + if DMWXFREQ_mapping.keys() != DMWXCOS_mapping.keys(): + raise ValueError( + "DMWXFREQ_ parameters do not match DMWXCOS_ parameters." + "Please check your prefixed parameters" + ) + # if len(DMWXFREQ_mapping.keys()) != len(DMWXSIN_mapping.keys()): + # raise ValueError( + # "The number of DMWXFREQ_ parameters do not match the number of DMWXSIN_ parameters." + # "Please check your prefixed parameters" + # ) + # if len(DMWXFREQ_mapping.keys()) != len(DMWXCOS_mapping.keys()): + # raise ValueError( + # "The number of DMWXFREQ_ parameters do not match the number of DMWXCOS_ parameters." + # "Please check your prefixed parameters" + # ) + if DMWXSIN_mapping.keys() != DMWXCOS_mapping.keys(): + raise ValueError( + "DMWXSIN_ parameters do not match DMWXCOS_ parameters." + "Please check your prefixed parameters" + ) + if len(DMWXSIN_mapping.keys()) != len(DMWXCOS_mapping.keys()): + raise ValueError( + "The number of DMWXSIN_ and DMWXCOS_ parameters do not match" + "Please check your prefixed parameters" + ) + wfreqs = np.zeros(len(DMWXFREQ_mapping)) + for j, index in enumerate(DMWXFREQ_mapping): + if (getattr(self, f"DMWXFREQ_{index:04d}").value == 0) or ( + getattr(self, f"DMWXFREQ_{index:04d}").quantity is None + ): + raise ValueError( + f"DMWXFREQ_{index:04d} is zero or None. Please check your prefixed parameters" + ) + if getattr(self, f"DMWXFREQ_{index:04d}").value < 0.0: + warn(f"Frequency DMWXFREQ_{index:04d} is negative") + wfreqs[j] = getattr(self, f"DMWXFREQ_{index:04d}").value + wfreqs.sort() + if np.any(np.diff(wfreqs) <= (1.0 / (2.0 * 364.25))): + warn("Frequency resolution is greater than 1/yr") + if self.DMWXEPOCH.value is None: + if self._parent is not None: + if self._parent.PEPOCH.value is None: + raise MissingParameter( + "DMWXEPOCH or PEPOCH are required if DMWaveX is being used" + ) + else: + self.DMWXEPOCH.quantity = self._parent.PEPOCH.quantity + + def validate_toas(self, toas): + return super().validate_toas(toas) + + def dmwavex_dm(self, toas): + total_dm = np.zeros(toas.ntoas) * dmu + dmwave_freqs = self.get_prefix_mapping_component("DMWXFREQ_") + dmwave_sins = self.get_prefix_mapping_component("DMWXSIN_") + dmwave_cos = self.get_prefix_mapping_component("DMWXCOS_") + + base_phase = toas.table["tdbld"].data * u.d - self.DMWXEPOCH.value * u.d + for idx, param in dmwave_freqs.items(): + freq = getattr(self, param).quantity + dmwxsin = getattr(self, dmwave_sins[idx]).quantity + dmwxcos = getattr(self, dmwave_cos[idx]).quantity + arg = 2.0 * np.pi * freq * base_phase + total_dm += dmwxsin * np.sin(arg.value) + dmwxcos * np.cos(arg.value) + return total_dm + + def dmwavex_delay(self, toas, acc_delay=None): + return self.dispersion_type_delay(toas) + + def d_dmwavex_delay_d_DMWXSIN(self, toas, param, delays, acc_delay=None): + par = getattr(self, param) + freq = getattr(self, f"DMWXFREQ_{int(par.index):04d}").quantity + base_phase = toas.table["tdbld"].data * u.d - self.DMWXEPOCH.value * u.d + arg = 2.0 * np.pi * freq * base_phase + deriv = np.sin(arg.value) + return deriv * u.s / par.units + + def d_dmwavex_delay_d_DMWXCOS(self, toas, param, delays, acc_delay=None): + par = getattr(self, param) + freq = getattr(self, f"DMWXFREQ_{int(par.index):04d}").quantity + base_phase = toas.table["tdbld"].data * u.d - self.DMWXEPOCH.value * u.d + arg = 2.0 * np.pi * freq * base_phase + deriv = np.cos(arg.value) + return deriv * u.s / par.units From a140776ecbae2de9c2ba39aa8b970bede4f5563d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 14:15:01 -0500 Subject: [PATCH 128/144] dmwavex_setup --- src/pint/utils.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/pint/utils.py b/src/pint/utils.py index 8cfbd95bf..f75c0b7db 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1366,6 +1366,86 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): return model.components["WaveX"].get_indices() +def dmwavex_setup(model, T_span, freqs=None, n_freqs=None): + """ + Set-up a DMWaveX model based on either an array of user-provided frequencies or the wave number + frequency calculation. Sine and Cosine amplitudes are initially set to zero + + User specifies T_span and either freqs or n_freqs. This function assumes that the timing model does not already + have any WaveX components. See add_dmwavex_component() or add_dmwavex_components() to add components + to an existing DMWaveX model. + + Parameters + ---------- + + model : pint.models.timing_model.TimingModel + freqs : iterable of float or astropy.quantity.Quantity, None + User inputed base frequencies + n_freqs : int, None + Number of wave frequencies to calculate using the equation: freq_n = 2 * pi * n / T_span + Where n is the wave number, and T_span is the total time span of the toas in the fitter object + T_span : float, astropy.quantity.Quantity + Time span used to calculate nyquist frequency when using freqs + Time span used to calculate WaveX frequencies when using n_freqs + Usually to be set as the length of the timing baseline the model is being used for + + Returns + ------- + + indices : list + Indices that have been assigned to new WaveX components + """ + from pint.models.dmwavex import DMWaveX + + if (freqs is None) and (n_freqs is None): + raise ValueError( + "DMWaveX component base frequencies are not specified. " + "Please input either freqs or n_freqs" + ) + + if (freqs is not None) and (n_freqs is not None): + raise ValueError( + "Both freqs and n_freqs are specified. Only one or the other should be used" + ) + + if n_freqs <= 0: + raise ValueError("Must use a non-zero number of wave frequencies") + model.add_component(DMWaveX()) + if isinstance(T_span, u.quantity.Quantity): + T_span.to(u.d) + else: + T_span *= u.d + + nyqist_freq = 1.0 / (2.0 * T_span) + if freqs is not None: + if isinstance(freqs, u.quantity.Quantity): + freqs.to(u.d**-1) + else: + freqs *= u.d**-1 + if len(freqs) == 1: + model.DMWXFREQ_0001.quantity = freqs + else: + np.array(freqs) + freqs.sort() + if min(np.diff(freqs)) < nyqist_freq: + warnings.warn( + "DMWaveX frequency spacing is finer than frequency resolution of data" + ) + model.DMWXFREQ_0001.quantity = freqs[0] + model.components["DMWaveX"].add_dmwavex_components(freqs[1:]) + + if n_freqs is not None: + if n_freqs == 1: + wave_freq = 2.0 * np.pi / T_span + model.DMWXFREQ_0001.quantity = wave_freq + else: + wave_numbers = np.arange(1, n_freqs + 1) + wave_freqs = 2.0 * np.pi * wave_numbers / T_span + model.DMWXFREQ_0001.quantity = wave_freqs[0] + model.components["DMWaveX"].add_dmwavex_components(wave_freqs[1:]) + return model.components["DMWaveX"].get_indices() + + def _translate_wave_freqs(om, k): """ Use Wave model WAVEOM parameter to calculate a WaveX WXFREQ_ frequency parameter for wave number k From 972cbac2a3180177fc0f0c0f91752719b30bdb6b Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 14:48:09 -0500 Subject: [PATCH 129/144] fix deriv --- src/pint/models/dispersion_model.py | 2 +- src/pint/models/dmwavex.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pint/models/dispersion_model.py b/src/pint/models/dispersion_model.py index 40acfdaec..6ef81258e 100644 --- a/src/pint/models/dispersion_model.py +++ b/src/pint/models/dispersion_model.py @@ -101,7 +101,7 @@ def d_delay_d_dmparam(self, toas, param_name, acc_delay=None): bfreq = self._parent.barycentric_radio_freq(toas) except AttributeError: warn("Using topocentric frequency for dedispersion!") - bfreq = toas.table["freq"] + bfreq = toas.table["freq"].quantity param_unit = getattr(self, param_name).units d_dm_d_dmparam = np.zeros(toas.ntoas) * u.pc / u.cm**3 / param_unit for df in self.dm_deriv_funcs[param_name]: diff --git a/src/pint/models/dmwavex.py b/src/pint/models/dmwavex.py index 9af2f95b4..061bbae23 100644 --- a/src/pint/models/dmwavex.py +++ b/src/pint/models/dmwavex.py @@ -272,9 +272,11 @@ def setup(self): # Get DMWaveX mapping and register DMWXSIN and DMWXCOS derivatives for prefix_par in self.get_params_of_type("prefixParameter"): if prefix_par.startswith("DMWXSIN_"): - self.register_deriv_funcs(self.d_dmwavex_delay_d_DMWXSIN, prefix_par) + self.register_deriv_funcs(self.d_delay_d_dmparam, prefix_par) + self.register_dm_deriv_funcs(self.d_dm_d_DMWXSIN, prefix_par) if prefix_par.startswith("DMWXCOS_"): - self.register_deriv_funcs(self.d_dmwavex_delay_d_DMWXCOS, prefix_par) + self.register_deriv_funcs(self.d_delay_d_dmparam, prefix_par) + self.register_dm_deriv_funcs(self.d_dm_d_DMWXCOS, prefix_par) self.wave_freqs = list( self.get_prefix_mapping_component("DMWXFREQ_").keys() ) @@ -361,18 +363,18 @@ def dmwavex_dm(self, toas): def dmwavex_delay(self, toas, acc_delay=None): return self.dispersion_type_delay(toas) - def d_dmwavex_delay_d_DMWXSIN(self, toas, param, delays, acc_delay=None): + def d_dm_d_DMWXSIN(self, toas, param, acc_delay=None): par = getattr(self, param) freq = getattr(self, f"DMWXFREQ_{int(par.index):04d}").quantity base_phase = toas.table["tdbld"].data * u.d - self.DMWXEPOCH.value * u.d arg = 2.0 * np.pi * freq * base_phase deriv = np.sin(arg.value) - return deriv * u.s / par.units + return deriv * dmu / par.units - def d_dmwavex_delay_d_DMWXCOS(self, toas, param, delays, acc_delay=None): + def d_dm_d_DMWXCOS(self, toas, param, acc_delay=None): par = getattr(self, param) freq = getattr(self, f"DMWXFREQ_{int(par.index):04d}").quantity base_phase = toas.table["tdbld"].data * u.d - self.DMWXEPOCH.value * u.d arg = 2.0 * np.pi * freq * base_phase deriv = np.cos(arg.value) - return deriv * u.s / par.units + return deriv * dmu / par.units From 72db594e808162cf3e574146256d35a16bc084fe Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 15:09:46 -0500 Subject: [PATCH 130/144] test --- src/pint/models/dmwavex.py | 4 ++-- tests/test_dmwavex.py | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/test_dmwavex.py diff --git a/src/pint/models/dmwavex.py b/src/pint/models/dmwavex.py index 061bbae23..a17499e53 100644 --- a/src/pint/models/dmwavex.py +++ b/src/pint/models/dmwavex.py @@ -277,10 +277,10 @@ def setup(self): if prefix_par.startswith("DMWXCOS_"): self.register_deriv_funcs(self.d_delay_d_dmparam, prefix_par) self.register_dm_deriv_funcs(self.d_dm_d_DMWXCOS, prefix_par) - self.wave_freqs = list( + self.dmwavex_freqs = list( self.get_prefix_mapping_component("DMWXFREQ_").keys() ) - self.num_wave_freqs = len(self.wave_freqs) + self.num_dmwavex_freqs = len(self.dmwavex_freqs) def validate(self): # Validate all the DMWaveX parameters diff --git a/tests/test_dmwavex.py b/tests/test_dmwavex.py new file mode 100644 index 000000000..5c178c4bc --- /dev/null +++ b/tests/test_dmwavex.py @@ -0,0 +1,40 @@ +from io import StringIO + +from pint.models import get_model +from pint.fitter import Fitter +from pint.simulation import make_fake_toas_uniform +from pint.utils import dmwavex_setup + +par = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB +""" + + +def test_dmwavex(): + m = get_model(StringIO(par)) + + idxs = dmwavex_setup(m, 3600, n_freqs=5) + + assert "DMWaveX" in m.components + assert m.num_dmwavex_freqs == len(idxs) + + m.components["DMWaveX"].remove_dmwavex_component(5) + assert m.num_dmwavex_freqs == len(idxs) - 1 + + t = make_fake_toas_uniform(54000, 56000, 200, m, add_noise=True) + + ftr = Fitter.auto(t, m) + ftr.fit_toas() + + assert ftr.resids.reduced_chi2 < 2 From 33bf20fdf561697945f3dd2c28f4953a36bfc147 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 15:11:14 -0500 Subject: [PATCH 131/144] CHANGELOG --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 2c0e0ffb9..2b37a870e 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -14,6 +14,7 @@ the released changes. - Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). ### Added - Added WaveX model as DelayComponent with wave amplitudes as fitted parameters +- Added DMWaveX model (Fourier representation of DM noise) ### Fixed - Wave model `validate()` can correctly use PEPOCH to assign WAVEEPOCH parameter - Fixed RTD by specifying theme explicitly. From 0a4c316593102909af18de5d59e779ab04d84b18 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 15:28:14 -0500 Subject: [PATCH 132/144] FIX TEST --- tests/test_wideband_dm_data.py | 46 +++++++++++++--------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/tests/test_wideband_dm_data.py b/tests/test_wideband_dm_data.py index c62d5d9f3..32df2af5d 100644 --- a/tests/test_wideband_dm_data.py +++ b/tests/test_wideband_dm_data.py @@ -60,19 +60,6 @@ def wb_model(tmpdir): return get_model(str(parfile)) -@pytest.fixture -def wb_toas(wb_model): - toas = get_TOAs(io.StringIO(tim)) - for i in range(9): - r = Residuals(toas, wb_model) - if np.all(r.time_resids < 1 * u.ns): - break - toas.adjust_TOAs(TimeDelta(-r.time_resids)) - else: - raise ValueError - return toas - - @pytest.fixture def wb_toas_all(wb_model): toas = get_TOAs(io.StringIO(tim_all)) @@ -162,14 +149,18 @@ def test_dmjump_derivative(self): assert self.model.dm_derivs[dmj_param.name] == [self.model.d_dm_d_dmjump] -def test_wideband_residuals(wb_model, wb_toas): - r = WidebandTOAResiduals(wb_toas, wb_model, dm_resid_args=dict(subtract_mean=False)) - assert len(r.toa.time_resids) == len(wb_toas) - assert len(r.dm.dm_data) < len(wb_toas) +def test_wideband_residuals(wb_model, wb_toas_all): + r = WidebandTOAResiduals( + wb_toas_all, wb_model, dm_resid_args=dict(subtract_mean=False) + ) + assert len(r.toa.time_resids) == len(wb_toas_all) + assert len(r.dm.dm_data) == len(wb_toas_all) -def test_wideband_residuals_dmjump(wb_model, wb_toas): - r = WidebandTOAResiduals(wb_toas, wb_model, dm_resid_args=dict(subtract_mean=False)) +def test_wideband_residuals_dmjump(wb_model, wb_toas_all): + r = WidebandTOAResiduals( + wb_toas_all, wb_model, dm_resid_args=dict(subtract_mean=False) + ) model = deepcopy(wb_model) assert wb_model.DMJUMP1.value == 0 model.DMJUMP1.value = 10 @@ -178,10 +169,17 @@ def test_wideband_residuals_dmjump(wb_model, wb_toas): model.DMJUMP0 with pytest.raises(AttributeError): model.DMJUMP2 - r2 = WidebandTOAResiduals(wb_toas, model, dm_resid_args=dict(subtract_mean=False)) + r2 = WidebandTOAResiduals( + wb_toas_all, model, dm_resid_args=dict(subtract_mean=False) + ) assert 0 < np.sum(r.dm.resids_value != r2.dm.resids_value) < len(r.dm.resids_value) +def test_read_mixed_timfile(): + with pytest.raises(ValueError): + get_TOAs(io.StringIO(tim)) + + def test_wideband_residuals_dof(wb_model, wb_toas_all): wb_model.free_params = ["DMJUMP1"] r = WidebandTOAResiduals( @@ -192,14 +190,6 @@ def test_wideband_residuals_dof(wb_model, wb_toas_all): assert_allclose(r.reduced_chi2, r.chi2 / r.dof) -@pytest.mark.xfail(reason="All TOAs must have DMs, currently") -def test_wideband_fit_dmjump(wb_model, wb_toas): - wb_model.free_params = ["DMJUMP1"] - fitter = WidebandTOAFitter(wb_toas, wb_model) - fitter.fit_toas() - assert_allclose(fitter.model.DMJUMP1.value, -10, atol=1e-3) - - def test_wideband_fit_dmjump_all(wb_model, wb_toas_all): wb_model.free_params = ["DMJUMP1"] fitter = WidebandTOAFitter(wb_toas_all, wb_model) From a8d1d0a7429275b255d95af68f82ad81118fdb7d Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 31 Aug 2023 17:17:28 -0500 Subject: [PATCH 133/144] test --- src/pint/models/dmwavex.py | 8 ++++---- tests/test_dmwavex.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pint/models/dmwavex.py b/src/pint/models/dmwavex.py index a17499e53..8f6ce8f57 100644 --- a/src/pint/models/dmwavex.py +++ b/src/pint/models/dmwavex.py @@ -81,9 +81,9 @@ def add_dmwavex_component( ) if isinstance(dmwxsin, u.quantity.Quantity): - dmwxsin = dmwxsin.to_value(u.s) + dmwxsin = dmwxsin.to_value(dmu) if isinstance(dmwxcos, u.quantity.Quantity): - dmwxcos = dmwxcos.to_value(u.s) + dmwxcos = dmwxcos.to_value(dmu) if isinstance(dmwxfreq, u.quantity.Quantity): dmwxfreq = dmwxfreq.to_value(1 / u.d) self.add_param( @@ -193,9 +193,9 @@ def add_dmwavex_components( if isinstance(dmwxfreq, u.quantity.Quantity): dmwxfreq = dmwxfreq.to_value(u.d**-1) if isinstance(dmwxsin, u.quantity.Quantity): - dmwxsin = dmwxsin.to_value(u.s) + dmwxsin = dmwxsin.to_value(dmu) if isinstance(dmwxcos, u.quantity.Quantity): - dmwxcos = dmwxcos.to_value(u.s) + dmwxcos = dmwxcos.to_value(dmu) log.trace(f"Adding DMWXSIN_{i} and DMWXCOS_{i} at frequency DMWXFREQ_{i}") self.add_param( prefixParameter( diff --git a/tests/test_dmwavex.py b/tests/test_dmwavex.py index 5c178c4bc..53687d1a0 100644 --- a/tests/test_dmwavex.py +++ b/tests/test_dmwavex.py @@ -4,6 +4,10 @@ from pint.fitter import Fitter from pint.simulation import make_fake_toas_uniform from pint.utils import dmwavex_setup +from pint import dmu + +import pytest +import astropy.units as u par = """ PSR B1937+21 @@ -24,6 +28,9 @@ def test_dmwavex(): m = get_model(StringIO(par)) + with pytest.raises(ValueError): + idxs = dmwavex_setup(m, 3600) + idxs = dmwavex_setup(m, 3600, n_freqs=5) assert "DMWaveX" in m.components @@ -38,3 +45,19 @@ def test_dmwavex(): ftr.fit_toas() assert ftr.resids.reduced_chi2 < 2 + + +def test_add_dmwavex(): + m = get_model(StringIO(par)) + idxs = dmwavex_setup(m, 3600, n_freqs=5) + + with pytest.raises(ValueError): + m.components["DMWaveX"].add_dmwavex_component(1, index=5, dmwxsin=0, dmwxcos=0) + + m.components["DMWaveX"].add_dmwavex_component(1, index=6, dmwxsin=0, dmwxcos=0) + assert m.num_dmwavex_freqs == len(idxs) + 1 + + m.components["DMWaveX"].add_dmwavex_component( + 1 / u.day, index=7, dmwxsin=0 * dmu, dmwxcos=0 * dmu + ) + assert m.num_dmwavex_freqs == len(idxs) + 2 From e91908d0a985d130c662e85cfd1bd80195b3e872 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Fri, 1 Sep 2023 16:03:14 -0500 Subject: [PATCH 134/144] initial version --- src/pint/models/timing_model.py | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 1f02b8c75..ea511defc 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -799,6 +799,84 @@ def orbital_phase(self, barytimes, anom="mean", radians=True): # return with radian units or return as unitless cycles from 0-1 return anoms * u.rad if radians else anoms / (2 * np.pi) + def radial_position(self, barytimes): + """Return line-of-sight position at barycentric MJD times. + + Parameters + ---------- + barytimes: Time, TOAs, array-like, or float + MJD barycentric time(s). The times to compute the + orbital phases. Needs to be a barycentric time in TDB. + If a TOAs instance is passed, the barycentering will happen + automatically. If an astropy Time object is passed, it must + be in scale='tdb'. If an array-like object is passed or + a simple float, the time must be in MJD format. + + Raises + ------ + ValueError + If an astropy Time object is passed with scale!="tdb". + + Returns + ------- + array + The line-of-sight position + """ + # this should also updaate the binary instance + nu = self.orbital_phase(barytimes, anom="true") + b = self.components[ + [x for x in self.components.keys() if x.startswith("Binary")][0] + ] + bbi = b.binary_instance # shorthand + psi = nu + bbi.omega() + return ( + bbi.a1() * np.sin(psi) * (1 - bbi.ecc() ** 2) / (1 + bbi.ecc() * np.cos(nu)) + ) + + def radial_velocity(self, barytimes): + """Return line-of-sight velocity at barycentric MJD times. + + Parameters + ---------- + barytimes: Time, TOAs, array-like, or float + MJD barycentric time(s). The times to compute the + orbital phases. Needs to be a barycentric time in TDB. + If a TOAs instance is passed, the barycentering will happen + automatically. If an astropy Time object is passed, it must + be in scale='tdb'. If an array-like object is passed or + a simple float, the time must be in MJD format. + + Raises + ------ + ValueError + If an astropy Time object is passed with scale!="tdb". + + Returns + ------- + array + The line-of-sight velocity + + Notes + ----- + See [1]_ + + .. [1] Lorimer & Kramer, 2008, "The Handbook of Pulsar Astronomy", Eqn. 8.24 + """ + # this should also updaate the binary instance + nu = self.orbital_phase(barytimes, anom="true") + b = self.components[ + [x for x in self.components.keys() if x.startswith("Binary")][0] + ] + bbi = b.binary_instance # shorthand + psi = nu + bbi.omega() + return ( + 2 + * np.pi + * bbi.a1() + / (bbi.pb() * np.sqrt(1 - bbi.ecc() ** 2)) + * (np.cos(psi) + bbi.ecc() * np.cos(bbi.omega())) + ).cgs + def conjunction(self, baryMJD): """Return the time(s) of the first superior conjunction(s) after baryMJD. From 74506a917b92aa2ed8cd7104387ff98fac405765 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Fri, 1 Sep 2023 16:04:39 -0500 Subject: [PATCH 135/144] added some explanation --- src/pint/models/timing_model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index ea511defc..9183a4baa 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -834,7 +834,7 @@ def radial_position(self, barytimes): ) def radial_velocity(self, barytimes): - """Return line-of-sight velocity at barycentric MJD times. + """Return line-of-sight velocity of the pulsar at barycentric MJD times. Parameters ---------- @@ -858,6 +858,9 @@ def radial_velocity(self, barytimes): Notes ----- + This is the radial velocity of the pulsar. For the radial velocity of the companion, + this must be multiplied by -1 times the mass of the pulsar divided by the mass of the companion. + See [1]_ .. [1] Lorimer & Kramer, 2008, "The Handbook of Pulsar Astronomy", Eqn. 8.24 From 25f38cafe8cd685b414b9741890e5448010e40ef Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 5 Sep 2023 09:16:25 -0500 Subject: [PATCH 136/144] Update CHANGELOG-unreleased.md --- CHANGELOG-unreleased.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index f0082c65d..726dfaef5 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -9,17 +9,17 @@ the released changes. ## Unreleased ### Changed -- WAVE parameters can be added to a Wave model with `add_wave_component()` in wave.py +- `WAVE` parameters can be added to a `Wave` model with `add_wave_component()` in `wave.py` - Moved design matrix normalization code from `pint.fitter` to the new `pint.utils.normalize_designmatrix()` function. - Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). ### Added -- Added WaveX model as DelayComponent with wave amplitudes as fitted parameters +- Added `WaveX` model as a `DelayComponent` with Fourier amplitudes as fitted parameters ### Fixed - Wave model `validate()` can correctly use PEPOCH to assign WAVEEPOCH parameter - Fixed RTD by specifying theme explicitly. - `.value()` now works for pairParameters - Setting `model.PARAM1 = model.PARAM2` no longer overrides the name of `PARAM1` -- Fixed an incorrect docstring in pbprime() functions. +- Fixed an incorrect docstring in `pbprime()` functions. - Fix ICRS -> ECL conversion when parameter uncertainties are not set. - `get_TOAs` raises an exception upon finding mixed narrowband and wideband TOAs in a tim file. `TOAs.is_wideband` returns True only if *ALL* TOAs have the -pp_dm flag. ### Removed From 8690cb67ebf4fc8f8fcdc6b17fa1f06fd7bca5cb Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Tue, 5 Sep 2023 09:45:52 -0500 Subject: [PATCH 137/144] tests --- src/pint/models/dmwavex.py | 2 +- src/pint/models/wavex.py | 2 +- tests/test_dmwavex.py | 91 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/pint/models/dmwavex.py b/src/pint/models/dmwavex.py index 8f6ce8f57..bd51f8465 100644 --- a/src/pint/models/dmwavex.py +++ b/src/pint/models/dmwavex.py @@ -137,7 +137,7 @@ def add_dmwavex_components( dmwxcoses : iterable of float or astropy.quantity.Quantity Cosine amplitudes for DMWaveX components frozens : iterable of bool or bool - Indicates whether sine adn cosine amplitudes of DMwavex components will be fit + Indicates whether sine and cosine amplitudes of DMwavex components will be fit Returns ------- diff --git a/src/pint/models/wavex.py b/src/pint/models/wavex.py index 2861308c1..12286169f 100644 --- a/src/pint/models/wavex.py +++ b/src/pint/models/wavex.py @@ -151,7 +151,7 @@ def add_wavex_components( wxcoses : iterable of float or astropy.quantity.Quantity Cosine amplitudes for WaveX components frozens : iterable of bool or bool - Indicates whether sine adn cosine amplitudes of wavex components will be fit + Indicates whether sine and cosine amplitudes of wavex components will be fit Returns ------- diff --git a/tests/test_dmwavex.py b/tests/test_dmwavex.py index 53687d1a0..0b37877aa 100644 --- a/tests/test_dmwavex.py +++ b/tests/test_dmwavex.py @@ -1,5 +1,7 @@ from io import StringIO +import numpy as np + from pint.models import get_model from pint.fitter import Fitter from pint.simulation import make_fake_toas_uniform @@ -47,6 +49,67 @@ def test_dmwavex(): assert ftr.resids.reduced_chi2 < 2 +def test_dmwavex_badpar(): + with pytest.raises(ValueError): + par = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB + DMWXFREQ_0001 0.01 + DMWXSIN_0001 0 + DMWXSIN_0002 0 + """ + get_model(StringIO(par)) + + with pytest.raises(ValueError): + par = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB + DMWXFREQ_0001 0.01 + DMWXCOS_0001 0 + DMWXCOS_0002 0 + """ + get_model(StringIO(par)) + + with pytest.raises(ValueError): + par = """ + PSR B1937+21 + LAMBDA 301.9732445337270 + BETA 42.2967523367957 + PMLAMBDA -0.0175 + PMBETA -0.3971 + PX 0.1515 + POSEPOCH 55321.0000 + F0 641.9282333345536244 1 0.0000000000000132 + F1 -4.330899370129D-14 1 2.149749089617D-22 + PEPOCH 55321.000000 + DM 71.016633 + UNITS TDB + DMWXFREQ_0001 0.00 + DMWXCOS_0001 0 + """ + get_model(StringIO(par)) + + def test_add_dmwavex(): m = get_model(StringIO(par)) idxs = dmwavex_setup(m, 3600, n_freqs=5) @@ -61,3 +124,31 @@ def test_add_dmwavex(): 1 / u.day, index=7, dmwxsin=0 * dmu, dmwxcos=0 * dmu ) assert m.num_dmwavex_freqs == len(idxs) + 2 + + m.components["DMWaveX"].add_dmwavex_component(2 / u.day) + assert m.num_dmwavex_freqs == len(idxs) + 3 + + m.components["DMWaveX"].add_dmwavex_components( + np.array([3]) / u.day, + dmwxsins=np.array([0]) * dmu, + dmwxcoses=np.array([0]) * dmu, + ) + assert m.num_dmwavex_freqs == len(idxs) + 4 + + with pytest.raises(ValueError): + m.components["DMWaveX"].add_dmwavex_components( + [2 / u.day, 3 / u.day], dmwxsins=[0, 0], dmwxcoses=[0, 0, 0] + ) + + with pytest.raises(ValueError): + m.components["DMWaveX"].add_dmwavex_components( + [2 / u.day, 3 / u.day], dmwxsins=[0, 0, 0], dmwxcoses=[0, 0] + ) + + with pytest.raises(ValueError): + m.components["DMWaveX"].add_dmwavex_components( + [2 / u.day, 3 / u.day], + dmwxsins=[0, 0], + dmwxcoses=[0, 0], + frozens=[False, False, False], + ) From 0cfaa5aa088f51ba2b92681a19a2041af2824ea3 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 5 Sep 2023 12:18:19 -0500 Subject: [PATCH 138/144] added tests --- tests/test_rv.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_rv.py diff --git a/tests/test_rv.py b/tests/test_rv.py new file mode 100644 index 000000000..78867705e --- /dev/null +++ b/tests/test_rv.py @@ -0,0 +1,43 @@ +import pytest +import numpy as np +from astropy import units as u, constants as c + +from pint.models import get_model +import pint.simulation +import pint.binaryconvert +from pinttestdata import datadir +import os + + +class TestRV: + def setup_method(self): + self.m = get_model( + os.path.join(datadir, "B1855+09_NANOGrav_dfg+12_modified_DD.par") + ) + # set the eccentricity to nonzero + # but not too high so that the models with various approximations won't fail + self.m.ECC.value = 0.01 + self.ts = self.m.T0.value + np.linspace(0, 2 * self.m.PB.value, 101) + self.ts = pint.simulation.make_fake_toas_fromMJDs(self.ts, self.m) + + self.v = self.m.radial_velocity(self.ts) + + def test_rv_basemodel(self): + nu = self.m.orbital_phase(self.ts, anom="true", radians=True) + # HBOPA 8.24 + v = ( + (2 * np.pi / self.m.PB.quantity) + * (self.m.A1.quantity / np.sqrt(1 - self.m.ECC.quantity**2)) + * ( + np.cos(nu + self.m.OM.quantity) + + self.m.ECC.quantity * np.cos(self.m.OM.quantity) + ) + ) + assert np.allclose(self.v, v) + + @pytest.mark.parametrize("othermodel", ["ELL1", "ELL1H", "DDS", "BT"]) + def test_rv_othermodels(self, othermodel): + mc = pint.binaryconvert.convert_binary(self.m, othermodel) + vc = mc.radial_velocity(self.ts) + # have a generous tolerance here since some of the models don't work well for high ECC + assert np.allclose(vc, self.v, atol=20 * u.km / u.s, rtol=1e-2) From 7156eff57db4ee241304208f5de6526595db51e3 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 5 Sep 2023 12:18:57 -0500 Subject: [PATCH 139/144] changelog --- CHANGELOG-unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index f0082c65d..c53d67941 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -14,6 +14,7 @@ the released changes. - Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). ### Added - Added WaveX model as DelayComponent with wave amplitudes as fitted parameters +- Added radial velocity method for binary models ### Fixed - Wave model `validate()` can correctly use PEPOCH to assign WAVEEPOCH parameter - Fixed RTD by specifying theme explicitly. From fd90fcc8ba8c48260bbe99b5b3ae5855b60de2d2 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 5 Sep 2023 14:32:50 -0500 Subject: [PATCH 140/144] added method for companion as well --- CHANGELOG-unreleased.md | 2 +- src/pint/models/timing_model.py | 43 ++++++++++++++++++++++++++++++--- tests/test_rv.py | 4 +-- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 8ef61d444..f00179747 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -14,7 +14,7 @@ the released changes. - Made `Residuals` independent of `GLSFitter` (GLS chi2 is now computed using the new function `Residuals._calc_gls_chi2()`). ### Added - Added WaveX model as DelayComponent with wave amplitudes as fitted parameters -- Added radial velocity method for binary models +- Added radial velocity methods for binary models ### Fixed - Wave model `validate()` can correctly use PEPOCH to assign WAVEEPOCH parameter - Fixed RTD by specifying theme explicitly. diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 9183a4baa..f2b3dcff7 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -833,8 +833,8 @@ def radial_position(self, barytimes): bbi.a1() * np.sin(psi) * (1 - bbi.ecc() ** 2) / (1 + bbi.ecc() * np.cos(nu)) ) - def radial_velocity(self, barytimes): - """Return line-of-sight velocity of the pulsar at barycentric MJD times. + def pulsar_radial_velocity(self, barytimes): + """Return line-of-sight velocity of the pulsar relative to the system barycenter at barycentric MJD times. Parameters ---------- @@ -858,8 +858,7 @@ def radial_velocity(self, barytimes): Notes ----- - This is the radial velocity of the pulsar. For the radial velocity of the companion, - this must be multiplied by -1 times the mass of the pulsar divided by the mass of the companion. + This is the radial velocity of the pulsar. See [1]_ @@ -880,6 +879,42 @@ def radial_velocity(self, barytimes): * (np.cos(psi) + bbi.ecc() * np.cos(bbi.omega())) ).cgs + def companion_radial_velocity(self, barytimes, massratio): + """Return line-of-sight velocity of the companion relative to the system barycenter at barycentric MJD times. + + Parameters + ---------- + barytimes: Time, TOAs, array-like, or float + MJD barycentric time(s). The times to compute the + orbital phases. Needs to be a barycentric time in TDB. + If a TOAs instance is passed, the barycentering will happen + automatically. If an astropy Time object is passed, it must + be in scale='tdb'. If an array-like object is passed or + a simple float, the time must be in MJD format. + massratio : float + Ratio of pulsar mass to companion mass + + + Raises + ------ + ValueError + If an astropy Time object is passed with scale!="tdb". + + Returns + ------- + array + The line-of-sight velocity + + Notes + ----- + This is the radial velocity of the companion. + + See [1]_ + + .. [1] Lorimer & Kramer, 2008, "The Handbook of Pulsar Astronomy", Eqn. 8.24 + """ + return -self.pulsar_radial_velocity(barytimes) * massratio + def conjunction(self, baryMJD): """Return the time(s) of the first superior conjunction(s) after baryMJD. diff --git a/tests/test_rv.py b/tests/test_rv.py index 78867705e..6ea9d4293 100644 --- a/tests/test_rv.py +++ b/tests/test_rv.py @@ -20,7 +20,7 @@ def setup_method(self): self.ts = self.m.T0.value + np.linspace(0, 2 * self.m.PB.value, 101) self.ts = pint.simulation.make_fake_toas_fromMJDs(self.ts, self.m) - self.v = self.m.radial_velocity(self.ts) + self.v = self.m.pulsar_radial_velocity(self.ts) def test_rv_basemodel(self): nu = self.m.orbital_phase(self.ts, anom="true", radians=True) @@ -38,6 +38,6 @@ def test_rv_basemodel(self): @pytest.mark.parametrize("othermodel", ["ELL1", "ELL1H", "DDS", "BT"]) def test_rv_othermodels(self, othermodel): mc = pint.binaryconvert.convert_binary(self.m, othermodel) - vc = mc.radial_velocity(self.ts) + vc = mc.pulsar_radial_velocity(self.ts) # have a generous tolerance here since some of the models don't work well for high ECC assert np.allclose(vc, self.v, atol=20 * u.km / u.s, rtol=1e-2) From a7416b4fd0befdf66a199627c5767d2d09f214cf Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 5 Sep 2023 14:34:22 -0500 Subject: [PATCH 141/144] removed untested function --- src/pint/models/timing_model.py | 34 --------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index f2b3dcff7..81943395f 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -799,40 +799,6 @@ def orbital_phase(self, barytimes, anom="mean", radians=True): # return with radian units or return as unitless cycles from 0-1 return anoms * u.rad if radians else anoms / (2 * np.pi) - def radial_position(self, barytimes): - """Return line-of-sight position at barycentric MJD times. - - Parameters - ---------- - barytimes: Time, TOAs, array-like, or float - MJD barycentric time(s). The times to compute the - orbital phases. Needs to be a barycentric time in TDB. - If a TOAs instance is passed, the barycentering will happen - automatically. If an astropy Time object is passed, it must - be in scale='tdb'. If an array-like object is passed or - a simple float, the time must be in MJD format. - - Raises - ------ - ValueError - If an astropy Time object is passed with scale!="tdb". - - Returns - ------- - array - The line-of-sight position - """ - # this should also updaate the binary instance - nu = self.orbital_phase(barytimes, anom="true") - b = self.components[ - [x for x in self.components.keys() if x.startswith("Binary")][0] - ] - bbi = b.binary_instance # shorthand - psi = nu + bbi.omega() - return ( - bbi.a1() * np.sin(psi) * (1 - bbi.ecc() ** 2) / (1 + bbi.ecc() * np.cos(nu)) - ) - def pulsar_radial_velocity(self, barytimes): """Return line-of-sight velocity of the pulsar relative to the system barycenter at barycentric MJD times. From cad5c87fbed774aab961ada2800db639a8a2acf8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Thu, 7 Sep 2023 09:23:26 -0500 Subject: [PATCH 142/144] typo --- src/pint/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index f75c0b7db..cf3cad814 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1372,7 +1372,7 @@ def dmwavex_setup(model, T_span, freqs=None, n_freqs=None): frequency calculation. Sine and Cosine amplitudes are initially set to zero User specifies T_span and either freqs or n_freqs. This function assumes that the timing model does not already - have any WaveX components. See add_dmwavex_component() or add_dmwavex_components() to add components + have any DMWaveX components. See add_dmwavex_component() or add_dmwavex_components() to add components to an existing DMWaveX model. Parameters From cb8cdfdbe2a1e380ec983eec934023d5ab6fb2c1 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Thu, 7 Sep 2023 11:39:06 -0500 Subject: [PATCH 143/144] typo --- src/pint/models/timing_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 81943395f..6cc845412 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -830,7 +830,7 @@ def pulsar_radial_velocity(self, barytimes): .. [1] Lorimer & Kramer, 2008, "The Handbook of Pulsar Astronomy", Eqn. 8.24 """ - # this should also updaate the binary instance + # this should also update the binary instance nu = self.orbital_phase(barytimes, anom="true") b = self.components[ [x for x in self.components.keys() if x.startswith("Binary")][0] From 2fa5a13b3c8a30bf9a3f5471f69242bd9fed6c16 Mon Sep 17 00:00:00 2001 From: Abhimanyu Susobhanan Date: Wed, 20 Sep 2023 14:47:44 -0500 Subject: [PATCH 144/144] assignment --- src/pint/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pint/utils.py b/src/pint/utils.py index cf3cad814..785ae36be 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -1345,7 +1345,7 @@ def wavex_setup(model, T_span, freqs=None, n_freqs=None): if len(freqs) == 1: model.WXFREQ_0001.quantity = freqs else: - np.array(freqs) + freqs = np.array(freqs) freqs.sort() if min(np.diff(freqs)) < nyqist_freq: warnings.warn( @@ -1425,7 +1425,7 @@ def dmwavex_setup(model, T_span, freqs=None, n_freqs=None): if len(freqs) == 1: model.DMWXFREQ_0001.quantity = freqs else: - np.array(freqs) + freqs = np.array(freqs) freqs.sort() if min(np.diff(freqs)) < nyqist_freq: warnings.warn(