diff --git a/CHANGELOG-unreleased.md b/CHANGELOG-unreleased.md index a2deae5cd..1166b30e4 100644 --- a/CHANGELOG-unreleased.md +++ b/CHANGELOG-unreleased.md @@ -9,6 +9,8 @@ the released changes. ## Unreleased ### Changed +- Replaced `pint.utils.find_optimal_nharms` by a more general function `pint.noise_analysis.find_optimal_nharms` which optimizes the `Nharms` for multiple noise components simultaneously. +- Updated the example `rednoise-fit-example.py` ### Added ### Fixed ### Removed diff --git a/docs/examples/rednoise-fit-example.py b/docs/examples/rednoise-fit-example.py index f0ab81058..d1c313fad 100644 --- a/docs/examples/rednoise-fit-example.py +++ b/docs/examples/rednoise-fit-example.py @@ -18,6 +18,7 @@ # %% from pint import DMconst from pint.models import get_model +from pint.noise_analysis import find_optimal_nharms from pint.simulation import make_fake_toas_uniform from pint.logging import setup as setup_log from pint.fitter import WLSFitter @@ -102,7 +103,11 @@ m1 = deepcopy(m) m1.remove_component("PLRedNoise") -nharm_opt, d_aics = find_optimal_nharms(m1, t, "WaveX", 30) +aics, nharm_opt = find_optimal_nharms( + m1, t, include_components=["WaveX"], nharms_max=30 +) +d_aics = aics - np.min(aics) +nharm_opt = nharm_opt[0] print("Optimum no of harmonics = ", nharm_opt) @@ -120,7 +125,7 @@ plt.ylabel("AIC - AIC$_\\min{} + 1$") plt.legend() plt.yscale("log") -# plt.savefig("sim3-aic.pdf") +plt.show() # %% # Now create a new model with the optimum number of harmonics @@ -183,7 +188,7 @@ ) plt.axvline(fyr, color="black", ls="dotted") plt.axhline(0, color="grey", ls="--") -plt.ylabel("Fourier coeffs ($\mu$s)") +plt.ylabel("Fourier coeffs ($\\mu$s)") plt.xscale("log") plt.legend(fontsize=8) @@ -202,6 +207,7 @@ plt.xlabel("Frequency (Hz)") plt.axvline(fyr, color="black", ls="dotted", label="1 yr$^{-1}$") plt.legend() +plt.show() # %% [markdown] # Note the outlier in the 1 year^-1 bin. This is caused by the covariance with RA and DEC, which introduce a delay with the same frequency. @@ -261,7 +267,12 @@ m2 = deepcopy(m1) -nharm_opt, d_aics = find_optimal_nharms(m2, t, "DMWaveX", 30) +aics, nharm_opt = find_optimal_nharms( + m2, t, include_components=["DMWaveX"], nharms_max=30 +) +d_aics = aics - np.min(aics) +nharm_opt = nharm_opt[0] + print("Optimum no of harmonics = ", nharm_opt) # %% @@ -275,7 +286,7 @@ plt.ylabel("AIC - AIC$_\\min{} + 1$") plt.legend() plt.yscale("log") -# plt.savefig("sim3-aic.pdf") +plt.show() # %% # Now create a new model with the optimum number of @@ -353,7 +364,7 @@ ) plt.axvline(fyr, color="black", ls="dotted") plt.axhline(0, color="grey", ls="--") -plt.ylabel("Fourier coeffs ($\mu$s)") +plt.ylabel("Fourier coeffs ($\\mu$s)") plt.xscale("log") plt.legend(fontsize=8) @@ -372,6 +383,7 @@ plt.xlabel("Frequency (Hz)") plt.axvline(fyr, color="black", ls="dotted", label="1 yr$^{-1}$") plt.legend() +plt.show() # %% [markdown] diff --git a/requirements.txt b/requirements.txt index 293e3d07a..4b207e7d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ uncertainties loguru nestle>=0.2.0 numdifftools +joblib diff --git a/src/pint/noise_analysis.py b/src/pint/noise_analysis.py new file mode 100644 index 000000000..61060a318 --- /dev/null +++ b/src/pint/noise_analysis.py @@ -0,0 +1,230 @@ +from copy import deepcopy +from typing import List, Optional, Tuple +from itertools import product as cartesian_product + +from joblib import Parallel, cpu_count, delayed +import numpy as np +from astropy import units as u + +from pint.models.chromatic_model import ChromaticCM +from pint.models.dispersion_model import DispersionDM +from pint.models.phase_offset import PhaseOffset +from pint.models.timing_model import TimingModel +from pint.toa import TOAs +from pint.logging import setup as setup_log +from pint.utils import ( + akaike_information_criterion, + cmwavex_setup, + dmwavex_setup, + wavex_setup, +) + + +def find_optimal_nharms( + model: TimingModel, + toas: TOAs, + include_components: List[str] = ["WaveX", "DMWaveX", "CMWaveX"], + nharms_max: int = 45, + chromatic_index: float = 4, + num_parallel_jobs: Optional[int] = None, +) -> Tuple[tuple, np.ndarray]: + """Find the optimal number of harmonics for `WaveX`/`DMWaveX`/`CMWaveX` using the + Akaike Information Criterion. + + This function runs a brute force search over a grid of harmonic numbers, from 0 to + `nharms_max`. This is executed in multiple processes using the `joblib` library the + number of processes is controlled through the `num_parallel_jobs` argument. + + Please note that the execution time scales as `O(nharms_max**len(include_components))`, + which can quickly become large. Hence, if you are using large values of `nharms_max`, it + is recommended that this be run on a cluster with a large number of CPUs. + + Parameters + ---------- + model: `pint.models.timing_model.TimingModel` + The timing model. Should not already contain `WaveX`/`DMWaveX` or `PLRedNoise`/`PLDMNoise`. + toas: `pint.toa.TOAs` + Input TOAs + component: list[str] + Component names; a non-empty sublist of ["WaveX", "DMWaveX", "CMWaveX"] + nharms_max: int, optional + Maximum number of harmonics (default is 45) for each component + chromatic_index: float + Chromatic index for `CMWaveX` + num_parallel_jobs: int, optional + Number of parallel processes. The default is the number of available CPU cores. + + Returns + ------- + aics: ndarray + Array of AIC values. + nharms_opt: tuple + Optimal numbers of harmonics + """ + assert len(set(include_components).intersection(set(model.components.keys()))) == 0 + assert len(include_components) > 0 + + idxs = list( + cartesian_product( + *np.repeat([np.arange(nharms_max + 1)], len(include_components), axis=0) + ) + ) + + if num_parallel_jobs is None: + num_parallel_jobs = cpu_count() + + aics_flat = Parallel(n_jobs=num_parallel_jobs, verbose=13)( + delayed( + lambda ii: compute_aic(model, toas, include_components, ii, chromatic_index) + )(ii) + for ii in idxs + ) + + aics = np.reshape(aics_flat, [nharms_max + 1] * len(include_components)) + + assert np.isfinite(aics).all(), "Infs/NaNs found in AICs!" + + return aics, np.unravel_index(np.argmin(aics), aics.shape) + + +def compute_aic( + model: TimingModel, + toas: TOAs, + include_components: List[str], + nharms: np.ndarray, + chromatic_index: float, +): + """Given a pre-fit model and TOAs, add the `[CM|DM]WaveX` components to the model, + fit the model to the TOAs, and compute the Akaike Information criterion using the + post-fit timing model. + + Parameters + ---------- + model: `pint.models.timing_model.TimingModel` + The pre-fit timing model. Should not already contain `WaveX`/`DMWaveX` or `PLRedNoise`/`PLDMNoise`. + toas: `pint.toa.TOAs` + Input TOAs + component: list[str] + Component names; a non-empty sublist of ["WaveX", "DMWaveX", "CMWaveX"] + nharms: ndarray + The number of harmonics for each component + chromatic_index: float + Chromatic index for `CMWaveX` + + Returns + ------- + aic: float + The AIC value. + """ + setup_log(level="WARNING") + + model1 = prepare_model( + model, toas.get_Tspan(), include_components, nharms, chromatic_index + ) + + from pint.fitter import Fitter + + # Downhill fitters don't work well here. + # TODO: Investigate this. + ftr = Fitter.auto(toas, model1, downhill=False) + ftr.fit_toas(maxiter=10) + + return akaike_information_criterion(ftr.model, toas) + + +def prepare_model( + model: TimingModel, + Tspan: u.Quantity, + include_components: List[str], + nharms: np.ndarray, + chromatic_index: float, +): + """Given a pre-fit model and TOAs, add the `[CM|DM]WaveX` components to the model. Also sets parameters like + `PHOFF` and `DM` and `CM` derivatives as free. + + Parameters + ---------- + model: `pint.models.timing_model.TimingModel` + The pre-fit timing model. Should not already contain `WaveX`/`DMWaveX` or `PLRedNoise`/`PLDMNoise`. + Tspan: u.Quantity + The observation time span + component: list[str] + Component names; a non-empty sublist of ["WaveX", "DMWaveX", "CMWaveX"] + nharms: ndarray + The number of harmonics for each component + chromatic_index: float + Chromatic index for `CMWaveX` + + Returns + ------- + aic: float + The AIC value. + """ + + model1 = deepcopy(model) + + for comp in ["PLRedNoise", "PLDMNoise", "PLCMNoise"]: + if comp in model1.components: + model1.remove_component(comp) + + if "PhaseOffset" not in model1.components: + model1.add_component(PhaseOffset()) + model1.PHOFF.frozen = False + + for jj, comp in enumerate(include_components): + if comp == "WaveX": + nharms_wx = nharms[jj] + if nharms_wx > 0: + wavex_setup(model1, Tspan, n_freqs=nharms_wx, freeze_params=False) + elif comp == "DMWaveX": + nharms_dwx = nharms[jj] + if nharms_dwx > 0: + if "DispersionDM" not in model1.components: + model1.add_component(DispersionDM()) + + model1["DM"].frozen = False + + if model1["DM1"].quantity is None: + model1["DM1"].quantity = 0 * model1["DM1"].units + model1["DM1"].frozen = False + + if "DM2" not in model1.params: + model1.components["DispersionDM"].add_param( + model["DM1"].new_param(2) + ) + if model1["DM2"].quantity is None: + model1["DM2"].quantity = 0 * model1["DM2"].units + model1["DM2"].frozen = False + + if model1["DMEPOCH"].quantity is None: + model1["DMEPOCH"].quantity = model1["PEPOCH"].quantity + + dmwavex_setup(model1, Tspan, n_freqs=nharms_dwx, freeze_params=False) + elif comp == "CMWaveX": + nharms_cwx = nharms[jj] + if nharms_cwx > 0: + if "ChromaticCM" not in model1.components: + model1.add_component(ChromaticCM()) + model1["TNCHROMIDX"].value = chromatic_index + + model1["CM"].frozen = False + if model1["CM1"].quantity is None: + model1["CM1"].quantity = 0 * model1["CM1"].units + model1["CM1"].frozen = False + + if "CM2" not in model1.params: + model1.components["ChromaticCM"].add_param( + model1["CM1"].new_param(2) + ) + if model1["CM2"].quantity is None: + model1["CM2"].quantity = 0 * model1["CM2"].units + model1["CM2"].frozen = False + + if model1["CMEPOCH"].quantity is None: + model1["CMEPOCH"].quantity = model1["PEPOCH"].quantity + + cmwavex_setup(model1, Tspan, n_freqs=nharms_cwx, freeze_params=False) + else: + raise ValueError(f"Unsupported component {comp}.") + + return model1 diff --git a/src/pint/utils.py b/src/pint/utils.py index ff5fc5317..e959ec4b3 100644 --- a/src/pint/utils.py +++ b/src/pint/utils.py @@ -3359,73 +3359,3 @@ def plchromnoise_from_cmwavex( model1.TNCHROMGAM.uncertainty_value = gamma_err return model1 - - -def find_optimal_nharms( - model: "pint.models.TimingModel", - toas: "pint.toa.TOAs", - component: Literal["WaveX", "DMWaveX"], - nharms_max: int = 45, -) -> Tuple[int, np.ndarray]: - """Find the optimal number of harmonics for `WaveX`/`DMWaveX` using the Akaike Information - Criterion. - - Parameters - ---------- - model: `pint.models.timing_model.TimingModel` - The timing model. Should not already contain `WaveX`/`DMWaveX` or `PLRedNoise`/`PLDMNoise`. - toas: `pint.toa.TOAs` - Input TOAs - component: str - Component name; "WaveX" or "DMWaveX" - nharms_max: int - Maximum number of harmonics - - Returns - ------- - nharms_opt: int - Optimal number of harmonics - aics: ndarray - Array of normalized AIC values. - """ - from pint.fitter import Fitter - - assert component in ["WaveX", "DMWaveX"] - assert ( - component not in model.components - ), f"{component} is already included in the model." - assert ( - "PLRedNoise" not in model.components and "PLDMNoise" not in model.components - ), "PLRedNoise/PLDMNoise cannot be included in the model." - - model1 = deepcopy(model) - - ftr = Fitter.auto(toas, model1, downhill=False) - ftr.fit_toas(maxiter=5) - aics = [akaike_information_criterion(model1, toas)] - model1 = ftr.model - - T_span = toas.get_mjds().max() - toas.get_mjds().min() - setup_component = wavex_setup if component == "WaveX" else dmwavex_setup - setup_component(model1, T_span, n_freqs=1, freeze_params=False) - - for _ in range(nharms_max): - ftr = Fitter.auto(toas, model1, downhill=False) - ftr.fit_toas(maxiter=5) - aics.append(akaike_information_criterion(ftr.model, toas)) - - model1 = ftr.model - if component == "WaveX": - model1.components[component].add_wavex_component( - (len(model1.components[component].get_indices()) + 1) / T_span, - frozen=False, - ) - else: - model1.components[component].add_dmwavex_component( - (len(model1.components[component].get_indices()) + 1) / T_span, - frozen=False, - ) - - assert all(np.isfinite(aics)), "Infs/NaNs found in AICs!" - - return np.argmin(aics), np.array(aics) - np.min(aics) diff --git a/tests/test_optimal_nharms.py b/tests/test_optimal_nharms.py new file mode 100644 index 000000000..dfec390fe --- /dev/null +++ b/tests/test_optimal_nharms.py @@ -0,0 +1,280 @@ +from copy import deepcopy +from io import StringIO +import numpy as np +import astropy.units as u +import pytest +from pint.models.model_builder import get_model +from pint.noise_analysis import compute_aic, find_optimal_nharms, prepare_model +from pint.simulation import make_fake_toas_uniform + + +@pytest.fixture(scope="module") +def data_wx(): + par_sim_wx = """ + PSR SIM3 + RAJ 05:00:00 1 + DECJ 15:00:00 1 + PEPOCH 55000 + F0 100 1 + F1 -1e-15 1 + PHOFF 0 1 + DM 15 1 + TNREDAMP -12.5 + TNREDGAM 3.5 + TNREDC 10 + TZRMJD 55000 + TZRFRQ 1400 + TZRSITE gbt + UNITS TDB + EPHEM DE440 + CLOCK TT(BIPM2019) + """ + m = get_model(StringIO(par_sim_wx)) + + ntoas = 200 + toaerrs = np.random.uniform(0.5, 2.0, ntoas) * u.us + freqs = np.linspace(500, 1500, 2) * u.MHz + + t = make_fake_toas_uniform( + startMJD=54001, + endMJD=56001, + ntoas=ntoas, + model=m, + freq=freqs, + obs="gbt", + error=toaerrs, + add_noise=True, + add_correlated_noise=True, + name="fake", + include_bipm=True, + multi_freqs_in_epoch=True, + ) + + return m, t + + +@pytest.fixture(scope="module") +def data_dmwx(): + par_sim_dmwx = """ + PSR SIM3 + RAJ 05:00:00 1 + DECJ 15:00:00 1 + PEPOCH 55000 + F0 100 1 + F1 -1e-15 1 + PHOFF 0 1 + DM 15 1 + TNDMAMP -13 + TNDMGAM 3.5 + TNDMC 10 + TZRMJD 55000 + TZRFRQ 1400 + TZRSITE gbt + UNITS TDB + EPHEM DE440 + CLOCK TT(BIPM2019) + """ + + m = get_model(StringIO(par_sim_dmwx)) + + ntoas = 200 + toaerrs = np.random.uniform(0.5, 2.0, ntoas) * u.us + freqs = np.linspace(500, 1500, 4) * u.MHz + + t = make_fake_toas_uniform( + startMJD=54001, + endMJD=56001, + ntoas=ntoas, + model=m, + freq=freqs, + obs="gbt", + error=toaerrs, + add_noise=True, + add_correlated_noise=True, + name="fake", + include_bipm=True, + multi_freqs_in_epoch=True, + ) + + return m, t + + +@pytest.fixture(scope="module") +def data_cmwx(): + par_sim_cmwx = """ + PSR SIM3 + RAJ 05:00:00 1 + DECJ 15:00:00 1 + PEPOCH 55000 + F0 100 1 + F1 -1e-15 1 + PHOFF 0 1 + DM 15 1 + TNCHROMIDX 4 + CM 10 + TNCHROMAMP -13 + TNCHROMGAM 3.5 + TNCHROMC 10 + TZRMJD 55000 + TZRFRQ 1400 + TZRSITE gbt + UNITS TDB + EPHEM DE440 + CLOCK TT(BIPM2019) + """ + + m = get_model(StringIO(par_sim_cmwx)) + + ntoas = 200 + toaerrs = np.random.uniform(0.5, 2.0, ntoas) * u.us + freqs = np.linspace(500, 1500, 4) * u.MHz + + t = make_fake_toas_uniform( + startMJD=54001, + endMJD=56001, + ntoas=ntoas, + model=m, + freq=freqs, + obs="gbt", + error=toaerrs, + add_noise=True, + add_correlated_noise=True, + name="fake", + include_bipm=True, + multi_freqs_in_epoch=True, + ) + + return m, t + + +def test_find_optimal_nharms_wx(data_wx): + m, t = data_wx + + m1 = deepcopy(m) + + aics, nharm = find_optimal_nharms(m1, t, include_components=["WaveX"], nharms_max=7) + + assert np.size(nharm) == 1 + assert np.array(nharm).item() <= 7 + assert np.all(np.isfinite(aics)) + + +def test_find_optimal_nharms_dmwx(data_dmwx): + m, t = data_dmwx + + m1 = deepcopy(m) + + aics, nharm = find_optimal_nharms( + m1, t, include_components=["DMWaveX"], nharms_max=7 + ) + + assert np.size(nharm) == 1 + assert np.array(nharm).item() <= 7 + assert np.all(np.isfinite(aics)) + + +def test_find_optimal_nharms_cmwx(data_cmwx): + m, t = data_cmwx + + m1 = deepcopy(m) + + aics, nharm = find_optimal_nharms( + m1, t, include_components=["CMWaveX"], nharms_max=7 + ) + + assert np.size(nharm) == 1 + assert np.array(nharm).item() <= 7 + assert np.all(np.isfinite(aics)) + + +@pytest.mark.parametrize( + "component_names", + [ + ["WaveX"], + ["DMWaveX"], + ["CMWaveX"], + ["WaveX", "DMWaveX"], + ["WaveX", "CMWaveX"], + ["DMWaveX", "CMWaveX"], + ["WaveX", "DMWaveX", "CMWaveX"], + ], +) +def test_prepare_model_for_find_optimal_nharms(data_dmwx, component_names): + m0, t = data_dmwx + + m = prepare_model( + model=m0, + Tspan=10 * u.year, + include_components=component_names, + nharms=np.repeat(10, len(component_names)), + chromatic_index=4, + ) + + assert "PLDMNoise" not in m.components + + assert "PHOFF" in m.free_params + + assert ("WaveX" in component_names) == ("WaveX" in m.components) + assert ("DMWaveX" in component_names) == ("DMWaveX" in m.components) + assert ("CMWaveX" in component_names) == ("CMWaveX" in m.components) + + if "WaveX" in component_names: + assert len(m.components["WaveX"].get_indices()) == 10 + + if "DMWaveX" in component_names: + assert not m.DM.frozen and m.DM.quantity is not None + assert not m.DM1.frozen and m.DM1.quantity is not None + assert not m.DM2.frozen and m.DM2.quantity is not None + assert len(m.components["DMWaveX"].get_indices()) == 10 + + if "CMWaveX" in component_names: + assert not m.CM.frozen and m.CM.quantity is not None + assert not m.CM1.frozen and m.CM1.quantity is not None + assert not m.CM2.frozen and m.CM2.quantity is not None + assert len(m.components["CMWaveX"].get_indices()) == 10 + + +@pytest.mark.parametrize( + "component_names", + [ + ["WaveX"], + ["DMWaveX"], + ["CMWaveX"], + ["WaveX", "DMWaveX"], + ["WaveX", "CMWaveX"], + ["DMWaveX", "CMWaveX"], + ["WaveX", "DMWaveX", "CMWaveX"], + ], +) +def test_compute_aic(data_dmwx, component_names): + m, t = data_dmwx + assert np.isfinite( + compute_aic( + m, + t, + include_components=component_names, + nharms=np.array((8, 9, 10)), + chromatic_index=4, + ) + ) + + +@pytest.mark.parametrize( + "component_names", + [ + ["WaveX", "DMWaveX"], + ["WaveX", "CMWaveX"], + ["DMWaveX", "CMWaveX"], + ["WaveX", "DMWaveX", "CMWaveX"], + ], +) +def test_find_multiple_nharms(data_dmwx, component_names): + m, t = data_dmwx + + aics, nharm = find_optimal_nharms( + m, t, include_components=component_names, nharms_max=3, num_parallel_jobs=3 + ) + + assert np.size(nharm) == len(component_names) + assert np.all(np.array(nharm) <= 7) + assert np.all(np.isfinite(aics)) diff --git a/tests/test_wx2pl.py b/tests/test_wx2pl.py index 8f426f2e3..4259fd833 100644 --- a/tests/test_wx2pl.py +++ b/tests/test_wx2pl.py @@ -1,16 +1,17 @@ import pytest from pint.models import get_model +from pint.noise_analysis import compute_aic, prepare_model from pint.simulation import make_fake_toas_uniform from pint.fitter import WLSFitter from pint.utils import ( cmwavex_setup, dmwavex_setup, - find_optimal_nharms, plchromnoise_from_cmwavex, wavex_setup, plrednoise_from_wavex, pldmnoise_from_dmwavex, ) +from pint.noise_analysis import find_optimal_nharms from io import StringIO import numpy as np @@ -18,7 +19,7 @@ from copy import deepcopy -@pytest.fixture +@pytest.fixture(scope="module") def data_wx(): par_sim_wx = """ PSR SIM3 @@ -63,7 +64,7 @@ def data_wx(): return m, t -@pytest.fixture +@pytest.fixture(scope="module") def data_dmwx(): par_sim_dmwx = """ PSR SIM3 @@ -109,7 +110,7 @@ def data_dmwx(): return m, t -@pytest.fixture +@pytest.fixture(scope="module") def data_cmwx(): par_sim_cmwx = """ PSR SIM3 @@ -163,7 +164,7 @@ def test_wx2pl(data_wx): m1 = deepcopy(m) m1.remove_component("PLRedNoise") - Tspan = t.get_mjds().max() - t.get_mjds().min() + Tspan = t.get_Tspan() wavex_setup(m1, Tspan, n_freqs=int(m.TNREDC.value), freeze_params=False) ftr = WLSFitter(t, m1) @@ -183,7 +184,7 @@ def test_dmwx2pldm(data_dmwx): m1 = deepcopy(m) m1.remove_component("PLDMNoise") - Tspan = t.get_mjds().max() - t.get_mjds().min() + Tspan = t.get_Tspan() dmwavex_setup(m1, Tspan, n_freqs=int(m.TNDMC.value), freeze_params=False) ftr = WLSFitter(t, m1) @@ -203,7 +204,7 @@ def test_cmwx2pldm(data_cmwx): m1 = deepcopy(m) m1.remove_component("PLChromNoise") - Tspan = t.get_mjds().max() - t.get_mjds().min() + Tspan = t.get_Tspan() cmwavex_setup(m1, Tspan, n_freqs=int(m.TNCHROMC.value), freeze_params=False) ftr = WLSFitter(t, m1) @@ -221,27 +222,3 @@ def test_cmwx2pldm(data_cmwx): abs(m.TNCHROMGAM.value - m2.TNCHROMGAM.value) / m2.TNCHROMGAM.uncertainty_value < 5 ) - - -def test_find_optimal_nharms_wx(data_wx): - m, t = data_wx - - m1 = deepcopy(m) - m1.remove_component("PLRedNoise") - - nharm, aics = find_optimal_nharms(m1, t, "WaveX", nharms_max=7) - - assert nharm <= 7 - assert np.all(aics >= 0) - - -def test_find_optimal_nharms_dmwx(data_dmwx): - m, t = data_dmwx - - m1 = deepcopy(m) - m1.remove_component("PLDMNoise") - - nharm, aics = find_optimal_nharms(m1, t, "DMWaveX", nharms_max=7) - - assert nharm <= 7 - assert np.all(aics >= 0)