diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index 6d7dae96c..9b2eb290f 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -15,6 +15,8 @@ the released changes. - `ssb_to_psb_ICRS` implementation is now a lot faster (uses erfa functions directly) - `ssb_to_psb_ECL` implementation within `AstrometryEcliptic` model is now a lot faster (uses erfa functions directly) - Upgraded versioneer for compatibility with Python 3.12 +- Creation of `Fitter` objects will fail if there are free unfittable parameters in the timing model. +- Only fittable parameters will be listed as check boxes in the `plk` interface. ### Added - CHI2, CHI2R, TRES, DMRES now in postfit par files - Added `WaveX` model as a `DelayComponent` with Fourier amplitudes as fitted parameters @@ -24,6 +26,7 @@ the released changes. - Support for wideband data in `pint.bayesian` (no correlated noise). - Added `DMWaveX` model (Fourier representation of DM noise) - Piecewise orbital model (`BinaryBTPiecewise`) +- `TimingModel.fittable_params` property - Simulate correlated noise using `pint.simulation` (also available via the `zima` script) ### Fixed - Wave model `validate()` can correctly use PEPOCH to assign WAVEEPOCH parameter @@ -33,4 +36,5 @@ the released changes. - 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. +- `TimingModel.designmatrix()` method will fail with an informative error message if there are free unfittable parameters in the timing model. ### Removed diff --git a/src/pint/fitter.py b/src/pint/fitter.py index 2e6a31a9b..ce678edc2 100644 --- a/src/pint/fitter.py +++ b/src/pint/fitter.py @@ -217,6 +217,16 @@ class Fitter: """ def __init__(self, toas, model, track_mode=None, residuals=None): + if not set(model.free_params).issubset(model.fittable_params): + free_unfittable_params = set(model.free_params).difference( + model.fittable_params + ) + raise ValueError( + f"Cannot create fitter because the following unfittable parameters " + f"were found unfrozen in the model: {free_unfittable_params}. " + f"Freeze these parameters before creating the fitter." + ) + self.toas = toas self.model_init = model self.track_mode = track_mode diff --git a/src/pint/models/binary_ell1.py b/src/pint/models/binary_ell1.py index 8e3c93b4e..01b619c0e 100644 --- a/src/pint/models/binary_ell1.py +++ b/src/pint/models/binary_ell1.py @@ -433,6 +433,11 @@ def __init__(self): self.binary_model_name = "ELL1k" self.binary_model_class = ELL1kmodel + self.remove_param("OMDOT") + self.remove_param("EDOT") + self.remove_param("EPS1DOT") + self.remove_param("EPS2DOT") + self.add_param( floatParameter( name="OMDOT", @@ -451,9 +456,6 @@ def __init__(self): ) ) - self.remove_param("EPS1DOT") - self.remove_param("EPS2DOT") - def validate(self): """Validate parameters.""" super().validate() diff --git a/src/pint/models/timing_model.py b/src/pint/models/timing_model.py index 113ff7fa1..0b6350548 100644 --- a/src/pint/models/timing_model.py +++ b/src/pint/models/timing_model.py @@ -53,6 +53,7 @@ Parameter, boolParameter, floatParameter, + funcParameter, intParameter, maskParameter, strParameter, @@ -592,7 +593,8 @@ def params(self): @property_exists def free_params(self): - """List of all the free parameters in the timing model. Can be set to change which are free. + """List of all the free parameters in the timing model. + Can be set to change which are free. These are ordered as ``self.params`` does. @@ -615,6 +617,17 @@ def free_params(self, params): f"Parameter(s) are familiar but not in the model: {params}" ) + @property_exists + def fittable_params(self): + """List of parameters that are fittable, i.e., the parameters + which have a derivative implemented. These derivatives are usually + accessed via the `d_delay_d_param` and `d_phase_d_param` methods.""" + return [ + p + for p in self.params + if (p in self.phase_deriv_funcs or p in self.delay_deriv_funcs) + ] + def match_param_aliases(self, alias): """Return PINT parameter name corresponding to this alias. @@ -1965,16 +1978,38 @@ def designmatrix(self, toas, acc_delay=None, incfrozen=False, incoffset=True): units : astropy.units.Unit The units of the corresponding parts of the design matrix - Note - ---- - Here we have negative sign here. Since in pulsar timing - the residuals are calculated as (Phase - int(Phase)), which is different - from the conventional definition of least square definition (Data - model) - We decide to add minus sign here in the design matrix, so the fitter - keeps the conventional way. + Notes + ----- + 1. We have negative sign here. Since the residuals are calculated as + (Phase - int(Phase)) in pulsar timing, which is different from the conventional + definition of least square definition (Data - model), we have decided to add + a minus sign here in the design matrix so that the fitter keeps the conventional + sign. + + 2. Design matrix entries can be computed only for parameters for which the + derivatives are implemented. If a parameter without a derivative is unfrozen + while calling this method, it will raise an informative error, except in the + case of unfrozen noise parameters, which are simply ignored. """ noise_params = self.get_params_of_component_type("NoiseComponent") + + if ( + not set(self.free_params) + .difference(noise_params) + .issubset(self.fittable_params) + ): + free_unfittable_params = ( + set(self.free_params) + .difference(noise_params) + .difference(self.fittable_params) + ) + raise ValueError( + f"Cannot compute the design matrix because the following unfittable parameters " + f"were found unfrozen in the model: {free_unfittable_params}. " + f"Freeze these parameters before computing the design matrix." + ) + # unfrozen_noise_params = [ # param for param in noise_params if not getattr(self, param).frozen # ] @@ -2918,7 +2953,11 @@ def __init__(self): def __repr__(self): return "{}(\n {})".format( self.__class__.__name__, - ",\n ".join(str(getattr(self, p)) for p in self.params), + ",\n ".join( + str(getattr(self, p)) + for p in self.params + if not isinstance(p, funcParameter) + ), ) def setup(self): diff --git a/src/pint/pintk/plk.py b/src/pint/pintk/plk.py index 368e19599..3a32ad013 100644 --- a/src/pint/pintk/plk.py +++ b/src/pint/pintk/plk.py @@ -208,6 +208,7 @@ def addFitCheckBoxes(self, model): for p in model.components[comp].params if p not in pulsar.nofitboxpars and getattr(model, p).quantity is not None + and p in model.fittable_params ] # Don't bother showing components without any fittable parameters