Home Installation Walkthrough Pipeline modules Pipeline configuration Plotting tools Community guidelines

FAOM API documentation

foam.build_optimised_pattern

Selection of the theoretical pulsation patterns that best match the observations, with the possibility to optimise the rotation rate in the process through rescaling.

  1"""Selection of the theoretical pulsation patterns that best match the observations,
  2   with the possibility to optimise the rotation rate in the process through rescaling."""
  3
  4import logging
  5import multiprocessing
  6import sys
  7from functools import partial
  8from pathlib import Path
  9
 10import astropy.units as u
 11import matplotlib.pyplot as plt
 12import numpy as np
 13import pandas as pd
 14from lmfit import Minimizer, Parameters
 15
 16logger = logging.getLogger("logger.bop")
 17
 18
 19################################################################################
 20def generate_spacing_series(periods, errors=None):
 21    """
 22    Generate the period spacing series (delta P = p_(n+1) - p_n )
 23
 24    Parameters
 25    ----------
 26    periods: list of floats
 27        Periods in units of days
 28     errors (optional): list of floats
 29        Errors on periods in units of days
 30    Returns
 31    ----------
 32    observed_spacings, observed_spacings_errors: tuple of lists of floats
 33        period spacing series (delta P values) and its errors (if supplied) in units of seconds
 34    """
 35    spacings = []
 36    if errors is None:
 37        spacings_errors = None
 38        for n, period_n in enumerate(periods[:-1]):
 39            spacings.append(abs(period_n - periods[n + 1]) * 86400.0)
 40    else:
 41        spacings_errors = []
 42        for n, period_n in enumerate(periods[:-1]):
 43            spacings.append(abs(period_n - periods[n + 1]) * 86400.0)
 44            spacings_errors.append(np.sqrt(errors[n] ** 2 + errors[n + 1] ** 2) * 86400.0)
 45
 46    return spacings, spacings_errors
 47
 48
 49################################################################################
 50def construct_theoretical_puls_pattern(
 51    pulsation_grid_file,
 52    observations_file,
 53    method_build_series,
 54    pattern_starting_pulsation=[],
 55    which_observable="period",
 56    output_file="theoretical_frequency_patterns.hdf",
 57    asymptotic_object=None,
 58    estimated_rotation=None,
 59    grid_parameters=None,
 60    nr_cpu=None,
 61):
 62    """
 63    Construct the theoretical frequency pattern for each model in the grid, which correspond to the observed pattern.
 64    (Each theoretical model is a row in 'pulsation_grid_file'.)
 65    The rotation rate will be scaled and optimised for each theoretical pattern individually.
 66    This optimisation will not be performed if asymptotic_object=None.
 67
 68    Parameters
 69    ----------
 70    pulsation_grid_file: string
 71        path to file containing input parameters of the models, and the pulsation frequencies of those models
 72        (as generated by function 'extract_frequency_grid' in 'functions_for_gyre').
 73    observations_file: string
 74        Path to the tsv file with observations, with a column for each observable and each set of errors.
 75        Column names specify the observable, and "_err" suffix denotes that it's the error.
 76    method_build_series: string
 77        way to generate the theoretical frequency pattern from each model to match the observed pattern. Options are:
 78            provided-pulsation: build pattern from the provided pulsation    (function 'puls_series_from_given_puls')
 79            highest-frequency: build pattern from the observed highest frequency    (function 'puls_series_from_given_puls')
 80            chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations (function 'chisq_longest_sequence')
 81    pattern_starting_pulsation: list of floats
 82        Only needed if you set method_build_series=provided-pulsation
 83        Value of the pulsation to start building the pattern from, one for each separated part of the pattern.
 84        The unit of this value needs to be the same as the observable set through which_observable.
 85    which_observable: string
 86        Observable used in the theoretical pattern construction, options are 'frequency' or 'period'.
 87    output_file: string
 88        Name (can include a path) for the file containing all the pulsation frequencies of the grid.
 89    asymptotic_object: asymptotic (see 'gmode_rotation_scaling')
 90        Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
 91    estimated_rotation: float
 92        Estimation of the rotation rate of the star, used as initial value in the optimisation problem.
 93    grid_parameters: list of string
 94        List of the parameters in the theoretical grid.
 95    nr_cpu: int
 96        Number of worker processes to use in multiprocessing. The default 'None' will use the number returned by os.cpu_count().
 97    """
 98    # Read in the files with observed and theoretical frequencies as pandas DataFrames
 99    obs_dataframe = pd.read_table(observations_file, sep="\s+", header=0, index_col="index")
100    theory_dataframe = pd.read_hdf(pulsation_grid_file)
101
102    obs = np.asarray(obs_dataframe[which_observable])
103    obs_err = np.asarray(obs_dataframe[f"{which_observable}_err"])
104
105    # if frequency was filled in as 0, it indicates an interruption in the pattern
106    missing_puls = np.where(obs == 0)[0]
107    # remove values indicating interruptions in the pattern
108    obs_without_missing = obs[obs != 0]
109    # remove values indicating interruptions in the pattern
110    obs_err_without_missing = obs_err[obs_err != 0]
111    # Adjust indices for removed 0-values of missing frequencies
112    missing_puls = [missing_puls[i] - i for i in range(len(missing_puls))]
113
114    # split into different parts of the interrupted pattern
115    obs_pattern_parts = np.split(obs_without_missing, missing_puls)
116    obs_err_pattern_parts = np.split(obs_err_without_missing, missing_puls)
117
118    # partial function fixes all parameters of the function except for 1 that is iterated over in the multiprocessing pool.
119    theory_pattern_func = partial(
120        theoretical_pattern_from_dfrow,
121        obs=obs,
122        obs_pattern_parts=obs_pattern_parts,
123        obs_err_pattern_parts=obs_err_pattern_parts,
124        which_observable=which_observable,
125        method_build_series=method_build_series,
126        pattern_starting_pulsation=pattern_starting_pulsation,
127        grid_parameters=grid_parameters,
128        asymptotic_object=asymptotic_object,
129        estimated_rotation=estimated_rotation,
130        plot_rotation_optimisation=False,
131    )
132
133    # Make the output file directory
134    Path(Path(output_file).parent).mkdir(parents=True, exist_ok=True)
135    # Send the rows of the dataframe iteratively to a pool of processors to get the theoretical pattern for each model
136    with multiprocessing.Pool(nr_cpu) as p:
137        freqs = p.imap(theory_pattern_func, theory_dataframe.iterrows())
138        header_parameters = ["rot", "rot_err"] + grid_parameters
139
140        for i in range(1, obs_dataframe.shape[0] + 1):
141            if i - 1 in np.where(obs_dataframe.index == "f_missing")[0]:
142                f = f"{which_observable}_missing"
143            else:
144                f = f"{which_observable}{i}"
145            header_parameters.append(f.strip())
146
147        data = []
148        for line in freqs:
149            data.append(line)
150
151    df = pd.DataFrame(data=data, columns=header_parameters)
152    df.to_hdf(path_or_buf=output_file, key="selected_puls_grid", format="table", mode="w")
153
154
155# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
156def theoretical_pattern_from_dfrow(
157    summary_grid_row,
158    obs,
159    obs_pattern_parts,
160    obs_err_pattern_parts,
161    which_observable,
162    method_build_series,
163    pattern_starting_pulsation=[],
164    asymptotic_object=None,
165    estimated_rotation=None,
166    plot_rotation_optimisation=False,
167    grid_parameters=None,
168):
169    """
170    Extract model parameters and a theoretical pulsation pattern from a row of the dataFrame that contains all model parameters and pulsation frequencies.
171
172    Parameters
173    ----------
174    summary_grid_row: tuple, made of (int, pandas series)
175        tuple returned from pandas.iterrows(), first tuple entry is the row index of the pandas dataFrame
176        second tuple entry is a pandas series, containing a row from the pandas dataFrame.
177        (This row holds model parameters and pulsation frequencies.)
178    obs: numpy array, dtype=float
179        Array of observed frequencies or periods. (Ordered increasing in frequency.)
180    obs_pattern_parts : list of numpy array, dtype=float
181        Holds a numpy array (with the observed frequencies or periods) per split off part
182        of the observed pattern if it is interrupted.
183        The list contains only one array if the observed pattern is uninterrupted.
184    obs_err_pattern_parts : list of numpy array, dtype=float
185        The errors on the data in obs_pattern_parts.
186    which_observable: string
187        Which observables are used in the pattern building, options are 'frequency' or 'period'.
188    method_build_series: string
189        way to generate the theoretical frequency pattern from each model
190        to match the observed pattern. Options are:
191            provided-pulsation: build pattern from the provided pulsation    (function 'puls_series_from_given_puls')
192            highest-frequency: build pattern from the observed highest frequency    (function 'puls_series_from_given_puls')
193            chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations    (function 'chisq_longest_sequence')
194    pattern_starting_pulsation: list of floats
195        Only needed if you set method_build_series=provided-pulsation
196        Value of the pulsation to start building the pattern from, one for each separated part of the pattern.
197        The unit of this value needs to be the same as the observable set through which_observable.
198    asymptotic_object: asymptotic (see 'gmode_rotation_scaling')
199        Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
200    estimated_rotation: float
201        Estimation of the rotation rate of the star, used as initial value in the optimisation problem.
202    grid_parameters: list of string
203        List of the parameters in the theoretical grid.
204
205    Returns
206    ----------
207    list_out: list of float
208        The input parameters and pulsation frequencies of the theoretical pattern
209        (or periods, depending on 'which_observable').
210    """
211    # all keys containing n_pg (these are all the radial orders)
212    freqs = np.asarray(summary_grid_row[1].filter(like="n_pg"))
213    # array with radial orders
214    orders = np.asarray([int(o.replace("n_pg", "")) for o in summary_grid_row[1].filter(like="n_pg").index])
215    orders = orders[~np.isnan(freqs)]
216    # remove all entries that are NaN in the numpy array (for when the models have a different amount of computed modes)
217    freqs = freqs[~np.isnan(freqs)]
218
219    # Check if pattern_starting_pulsation has enough entries to not truncate other parts in the zip function.
220    if len(obs_pattern_parts) != len(pattern_starting_pulsation):
221        if method_build_series == "provided-pulsation":
222            sys.exit(
223                logger.error(
224                    "Amount of pulsations specified to build patterns from is not equal to the amount of split-off parts in the pattern."
225                )
226            )
227        # Content of pattern_starting_pulsation doesn't matter if it's not used to build the pattern.
228        else:
229            # We only care about the length if the method doesn't use specified pulsations.
230            pattern_starting_pulsation = [None] * len(obs_pattern_parts)
231
232    # In this case, rescaling nor optimisation will happen
233    if asymptotic_object is None:
234        residual = rescale_rotation_and_select_theoretical_pattern(
235            None,
236            asymptotic_object,
237            estimated_rotation,
238            freqs,
239            orders,
240            obs,
241            obs_pattern_parts,
242            obs_err_pattern_parts,
243            which_observable,
244            method_build_series,
245            pattern_starting_pulsation,
246        )
247
248        list_out = [estimated_rotation, 0]
249        for parameter in grid_parameters:
250            list_out.append(summary_grid_row[1][parameter])
251
252        selected_pulsations = obs + residual
253        list_out.extend(selected_pulsations)
254
255    else:
256        # Optimise the rotation rate and get the pulsations at that rotation rate
257        params = Parameters()
258        params.add("rotation", value=estimated_rotation, min=1e-5)
259        # Fit rotation to observed pattern with the default leastsq algorithm
260        optimise_rotation = Minimizer(
261            rescale_rotation_and_select_theoretical_pattern,
262            params,
263            fcn_args=(
264                asymptotic_object,
265                estimated_rotation,
266                freqs,
267                orders,
268                obs,
269                obs_pattern_parts,
270                obs_err_pattern_parts,
271                which_observable,
272                method_build_series,
273                pattern_starting_pulsation,
274            ),
275        )
276
277        result_minimizer = optimise_rotation.minimize()
278
279        # Search from second initial value, which is as separated from the first solution as the first initial guess.
280        search_second_initial_value = 2 * result_minimizer.params["rotation"].value - estimated_rotation
281        params = Parameters()
282        params.add("rotation", value=search_second_initial_value, min=1e-5)
283        optimise_rotation = Minimizer(
284            rescale_rotation_and_select_theoretical_pattern,
285            params,
286            fcn_args=(
287                asymptotic_object,
288                estimated_rotation,
289                freqs,
290                orders,
291                obs,
292                obs_pattern_parts,
293                obs_err_pattern_parts,
294                which_observable,
295                method_build_series,
296                pattern_starting_pulsation,
297            ),
298        )
299
300        result_minimizer2 = optimise_rotation.minimize()
301        # Select the minimizer with the initial guess that resulted in the best fit.
302        if result_minimizer2.chisqr < result_minimizer.chisqr:
303            result_minimizer = result_minimizer2
304
305        optimised_pulsations = result_minimizer.residual + obs
306
307        if result_minimizer.message != "Fit succeeded.":
308            logger.warning(
309                f"""Fitting rotation did not succeed: {result_minimizer.message}
310                            for model {summary_grid_row[1].drop(summary_grid_row[1].filter(regex='n_pg').index).drop('rot').to_dict()} using method: {method_build_series}
311                            rotation found: {result_minimizer.params['rotation'].value} with error: {result_minimizer.params['rotation'].stderr}"""
312            )
313
314        if plot_rotation_optimisation:
315            fig1, ax1 = plt.subplots()
316            if which_observable == "frequency":
317                obsperiod = 1 / obs
318                optimised_periods = 1 / optimised_pulsations
319            else:
320                obsperiod = obs
321                optimised_periods = optimised_pulsations
322
323            # Recombine observational errors in one array
324            combined_obs_err = []
325            for obs_err_part in obs_err_pattern_parts:
326                if len(combined_obs_err) > 0:
327                    combined_obs_err.extend([0])
328                combined_obs_err.extend(obs_err_part)
329            combined_obs_err = np.asarray(combined_obs_err)
330
331            spacings = generate_spacing_series(obsperiod, combined_obs_err)
332            ax1.errorbar(obsperiod[:-1], spacings[0], fmt="o", yerr=spacings[1], label="obs", color="blue", alpha=0.8)
333            ax1.plot(obsperiod[:-1], spacings[0], color="blue")
334            ax1.plot(
335                optimised_periods[:-1],
336                generate_spacing_series(optimised_periods)[0],
337                "*",
338                ls="solid",
339                color="orange",
340                label="optimised",
341            )
342            ax1.plot(
343                1 / freqs[:-1], generate_spacing_series(1 / freqs)[0], ".", ls="solid", label="initial", color="green"
344            )
345
346            fig2, ax2 = plt.subplots()
347
348            ax2.errorbar(obs, obs, fmt="o", xerr=combined_obs_err, label="obs", color="blue", alpha=0.8)
349            ax2.plot(optimised_periods, optimised_periods, "*", color="orange", label="optimised")
350            ax2.plot(1 / freqs, 1 / freqs, ".", label="initial", color="green")
351
352            ax1.legend(prop={"size": 14})
353            ax2.legend(prop={"size": 14})
354            ax1.set_title(
355                f"initial omega = {estimated_rotation}, optimised omega = {result_minimizer.params['rotation'].value}"
356            )
357            ax2.set_title(
358                f"initial omega = {estimated_rotation}, optimised omega = {result_minimizer.params['rotation'].value}"
359            )
360            plt.show()
361
362        # Create list with rotation, its error, all the input parameters, and the optimised pulsations
363        list_out = [result_minimizer.params["rotation"].value, result_minimizer.params["rotation"].stderr]
364        for parameter in grid_parameters:
365            list_out.append(summary_grid_row[1][parameter])
366        list_out.extend(optimised_pulsations)
367
368    return list_out
369
370
371# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
372def rescale_rotation_and_select_theoretical_pattern(
373    params,
374    asymptotic_object,
375    estimated_rotation,
376    freqs_input,
377    orders,
378    obs,
379    obs_pattern_parts,
380    obs_err_pattern_parts,
381    which_observable,
382    method_build_series,
383    pattern_starting_pulsation,
384):
385    """
386    If rotation is not optimised in the modelling, asymptotic_object should be set to 'None' and
387    this function will just select a theoretical pulsation pattern based on the specified method.
388    If asymptotic_object is supplied, the rotation will be optimised and this will be the
389    objective function for the lmfit Minimizer class to minimise the output of.
390    Rescales the theoretical pulsation pattern using the asymptotic object from
391    'gmode_rotation_scaling', and selects a theoretical pulsation pattern based
392    on the specified method.
393
394    Parameters
395    ----------
396    params: Parameter (object of lmfit)
397        The parameter used to optimise the fit. In this case rotation in units of 1/day.
398    asymptotic_object: asymptotic (see 'gmode_rotation_scaling')
399        Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
400    estimated_rotation: float
401        Estimated rotation rate in units of 1/day. Used as initial value in the optimisation problem.
402    freqs_input: numpy array, dtype=float
403        Array of the frequencies as computed by GYRE, to be scaled to the optimal rotation rate.
404    orders: numpy array, dtype=int
405        Array with the radial orders of the theoretical input frequencies.
406    obs: numpy array, dtype=float
407        Array of observed frequencies or periods. (Ordered increasing in frequency.)
408    obs_pattern_parts: list of numpy array, dtype=float
409        Holds a numpy array with observed frequencies or periods
410        per split off part of the observed pattern if it is interrupted.
411        The list contains only one array if the observed pattern is uninterrupted.
412    obs_err_pattern_parts: list of numpy array, dtype=float
413        The errors on obs_pattern_parts
414    which_observable: string
415        Which observables are used in the pattern building, options are 'frequency' or 'period'.
416    method_build_series: string
417        way to generate the theoretical frequency pattern from each model
418        to match the observed pattern. Options are:
419            provided-pulsation: build pattern from the provided pulsation    (function 'puls_series_from_given_puls')
420            highest-frequency: build pattern from the observed highest frequency    (function 'puls_series_from_given_puls')
421            chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations    (function 'chisq_longest_sequence')
422    pattern_starting_pulsation: list of floats
423        Only needed if you set method_build_series=provided-pulsation
424        Value of the pulsation to start building the pattern from, one for each separated part of the pattern.
425        The unit of this value needs to be the same as the observable set through which_observable.
426
427    Returns
428    ----------
429    (output_pulsations - obs): numpy array, dtype=float
430        Differences between the scaled pulsations and the observations.
431        The array to be minimised by the lmfit Minimizer if rotation is optimised.
432    """
433    if asymptotic_object is not None:
434        v = params.valuesdict()
435        # To avoid division by zero in scale_pattern
436        if estimated_rotation == 0:
437            estimated_rotation = 1e-99
438        freqs = asymptotic_object.scale_pattern(freqs_input / u.d, estimated_rotation / u.d, v["rotation"] / u.d) * u.d
439        # convert astropy quantities back to floats
440        freqs = np.asarray(freqs, dtype=np.float64)
441    else:
442        freqs = freqs_input
443
444    periods = 1 / freqs
445
446    output_pulsations = []
447    for obs_part, obs_err_part, pattern_starting_pulsation_part in zip(
448        obs_pattern_parts, obs_err_pattern_parts, pattern_starting_pulsation
449    ):
450        # To indicate interruptions in the pattern
451        if len(output_pulsations) > 0:
452            output_pulsations.append(0)
453
454        if which_observable == "frequency":
455            # remove frequencies that were already chosen in a different, split-off part of the pattern
456            if len(output_pulsations) > 0:
457                # If input is in increasing radial order (decreasing n_pg, since n_pg is negative for g-modes)
458                if orders[1] == orders[0] - 1:
459                    # index -2 to get lowest, non-zero freq
460                    np.delete(freqs, np.where(freqs >= output_pulsations[-2]))
461                # If input is in decreasing radial order
462                else:
463                    np.delete(freqs, np.where(freqs <= max(output_pulsations)))
464            theory_value = freqs
465            obs_period = 1 / obs_part
466            obs_period_err = obs_err_part / obs_part**2
467            highest_obs_freq = max(obs_part)
468
469        elif which_observable == "period":
470            # remove periods that were already chosen in a different, split-off part of the pattern
471            if len(output_pulsations) > 0:
472                # If input is in increasing radial order (decreasing n_pg, since n_pg is negative for g-modes)
473                if orders[1] == orders[0] - 1:
474                    np.delete(periods, np.where(periods <= max(output_pulsations)))
475                # If input is in decreasing radial order
476                else:
477                    # index -2 to get lowest, non-zero period
478                    np.delete(periods, np.where(periods >= output_pulsations[-2]))
479            theory_value = periods
480            obs_period = obs_part
481            obs_period_err = obs_err_part
482            # highest frequency is lowest period
483            highest_obs_freq = min(obs_part)
484        else:
485            sys.exit(logger.error("Unknown observable to fit"))
486
487        if method_build_series == "provided-pulsation":
488            selected_theoretical_pulsations = puls_series_from_given_puls(
489                theory_value, obs_part, pattern_starting_pulsation_part
490            )
491        elif method_build_series == "highest-frequency":
492            selected_theoretical_pulsations = puls_series_from_given_puls(theory_value, obs_part, highest_obs_freq)
493        elif method_build_series == "chisq-longest-sequence":
494            series_chi2, final_theoretical_periods, corresponding_orders = chisq_longest_sequence(
495                periods, orders, obs_period, obs_period_err
496            )
497            if which_observable == "frequency":
498                selected_theoretical_pulsations = 1 / np.asarray(final_theoretical_periods)
499            elif which_observable == "period":
500                selected_theoretical_pulsations = final_theoretical_periods
501        else:
502            sys.exit(logger.error(f"Unrecognised method to build pulsation series: {method_build_series}"))
503
504        output_pulsations.extend(selected_theoretical_pulsations)
505
506    return output_pulsations - obs
507
508
509################################################################################
510def puls_series_from_given_puls(theory_in, obs, obs_to_build_from, plot=False):
511    """
512    Generate a theoretical pulsation pattern (can be in frequency or period) from the given observations.
513    Build consecutively in radial order, starting from the theoretical value closest to the provided observational value.
514
515    Parameters
516    ----------
517    theory_in: numpy array, dtype=float
518        Array of theoretical frequencies or periods.
519    obs: numpy array, dtype=float
520        Array of observed frequencies or periods.
521    obs_to_build_from: float
522        observed frequency or period value to start building the pattern from.
523    plot: boolean
524        Make a period spacing diagram for the constructed series.
525
526    Returns
527    ----------
528    theory_sequence: list of float
529        The constructed theoretical frequency pattern
530    """
531    # get index of observation to build the series from
532    nth_obs = np.where(obs == obs_to_build_from)[0][0]
533    # search theoretical freq closest to the given observed one
534    diff = abs(theory_in - obs_to_build_from)
535    # get index of this theoretical frequency
536    index = np.where(diff == min(diff))[0][0]
537
538    # Insert a value of -1 if observations miss a theoretical counterpart in the beginning
539    theory_sequence = []
540    if (index - nth_obs) < 0:
541        for i in range(abs((index - nth_obs))):
542            theory_sequence.append(-1)
543        theory_sequence.extend(theory_in[0 : index + (len(obs) - nth_obs)])
544    else:
545        theory_sequence.extend(theory_in[index - nth_obs : index + (len(obs) - nth_obs)])
546
547    # Insert a value of -1 if observations miss a theoretical counterpart at the end
548    if index + (len(obs) - nth_obs) > len(theory_in):
549        for i in range((index + (len(obs) - nth_obs)) - len(theory_in)):
550            theory_sequence.append(-1)
551
552    if plot is True:
553        fig = plt.figure()
554        ax = fig.add_subplot(111)
555        theory = np.asarray(theory_sequence)
556        ax.plot((1 / obs)[::-1][:-1], np.diff((1 / obs)[::-1]) * 86400, "ko", lw=1.5, linestyle="-")
557        ax.plot(
558            (1.0 / theory)[::-1][:-1],
559            -np.diff(1.0 / theory)[::-1] * 86400,
560            "ko",
561            color="blue",
562            lw=1.5,
563            linestyle="--",
564            markersize=6,
565            markeredgewidth=0.0,
566        )
567        plt.show()
568
569    return theory_sequence
570
571
572################################################################################
573def chisq_longest_sequence(theory_periods, orders, obs_periods, obs_periods_errors):
574    """
575    Method to extract the theoretical pattern that best matches the observed one.
576    Match each observed mode period to its best matching theoretical counterpart,
577    and adopt the longest sequence of consecutive modes found this way.
578    In case of multiple mode series with the same length, a final pattern selection
579    is made based on the best (chi-square) match between theory and observations.
580
581    Parameters
582    ----------
583    theory_periods : numpy array, dtype=float
584        Theoretical periods and their radial orders.
585    orders : numpy array, dtype=int
586        Radial orders corresponding to the periods in theory_periods.
587    obs_periods: numpy array, dtype=float
588        Observational periods.
589    obs_periods_errors: numpy array, dtype=float
590        The errors on obs_periods.
591
592    Returns
593    ----------
594    series_chi2: float
595        chi2 value of the selected theoretical frequencies.
596    final_theoretical_periods: numpy array, dtype=float
597        The selected theoretical periods that best match the observed pattern.
598    corresponding_orders: list of integers
599        The radial orders of the returned theoretical periods.
600    """
601    if len(theory_periods) < len(obs_periods):
602        return 1e16, [-1.0 for i in range(len(obs_periods))], [-1 for i in range(len(obs_periods))]
603    else:
604        # Find the best matches per observed period
605        pairs_orders = []
606        for ii, period in enumerate(obs_periods):
607            ## Chi_squared array definition
608            chi_sqrs = np.array(
609                [((period - theory_period) / obs_periods_errors[ii]) ** 2 for theory_period in theory_periods]
610            )
611
612            ## Locate the theoretical frequency (and accompanying order) with the best chi2
613            min_ind = np.where(chi_sqrs == min(chi_sqrs))[0]
614            best_match = theory_periods[min_ind][0]
615            best_order = orders[min_ind][0]
616
617            ## Toss everything together for bookkeeping
618            pairs_orders.append([period, best_match, int(best_order), chi_sqrs[min_ind][0]])
619
620        pairs_orders = np.array(pairs_orders)
621
622        # If input is in increasing radial order (decreasing n_pg, since n_pg is negative for g-modes)
623        if orders[1] == orders[0] - 1:
624            increase_or_decrease = -1
625        # If input is in decreasing radial order
626        else:
627            increase_or_decrease = 1
628
629        sequences = []
630        ## Go through all pairs of obs and theoretical frequencies and
631        ## check if the next observed frequency has a corresponding theoretical frequency
632        ## with the consecutive radial order.
633        current = []
634        lp = len(pairs_orders[:-1])
635        for ii, sett in enumerate(pairs_orders[:-1]):
636            if abs(sett[2]) == abs(pairs_orders[ii + 1][2]) + increase_or_decrease:
637                current.append(sett)
638            # If not consecutive radial order, save the current sequence and start a new one.
639            else:
640                current.append(sett)
641                sequences.append(np.array(current).reshape(len(current), 4))
642                current = []
643            if ii == lp - 1:
644                current.append(sett)
645                sequences.append(np.array(current).reshape(len(current), 4))
646                current = []
647        len_list = np.array([len(x) for x in sequences])
648        longest = np.where(len_list == max(len_list))[0]
649
650        ## Test if there really is one longest sequence
651        if len(longest) == 1:
652            lseq = sequences[longest[0]]
653
654        ## if not, pick, of all the sequences with the same length, the best based on chi2
655        else:
656            scores = [np.sum(sequences[ii][:, -1]) / len(sequences[ii]) for ii in longest]
657            min_score = np.where(scores == min(scores))[0][0]
658            lseq = sequences[longest[min_score]]
659
660        obs_ordering_ind = np.where(obs_periods == lseq[:, 0][0])[0][0]
661        thr_ordering_ind = np.where(theory_periods == lseq[:, 1][0])[0][0]
662
663        ordered_theoretical_periods = []
664        corresponding_orders = []
665
666        thr_ind_start = thr_ordering_ind - obs_ordering_ind
667        thr_ind_current = thr_ind_start
668
669        for i, oper in enumerate(obs_periods):
670            thr_ind_current = thr_ind_start + i
671            if thr_ind_current < 0:
672                tper = -1
673                ordr = -1
674            elif thr_ind_current >= len(theory_periods):
675                tper = -1
676                ordr = -1
677            else:
678                tper = theory_periods[thr_ind_current]
679                ordr = orders[thr_ind_current]
680            ordered_theoretical_periods.append(tper)
681            corresponding_orders.append(ordr)
682
683        final_theoretical_periods = np.array(ordered_theoretical_periods)
684
685        obs_series, obs_series_errors = generate_spacing_series(obs_periods, obs_periods_errors)
686        thr_series, _ = generate_spacing_series(final_theoretical_periods)
687
688        obs_series = np.array(obs_series)
689        obs_series_errors = np.array(obs_series_errors)
690        thr_series = np.array(thr_series)
691
692        series_chi2 = np.sum(((obs_series - thr_series) / obs_series_errors) ** 2) / len(obs_series)
693
694        return series_chi2, final_theoretical_periods, corresponding_orders
def generate_spacing_series(periods, errors=None):
21def generate_spacing_series(periods, errors=None):
22    """
23    Generate the period spacing series (delta P = p_(n+1) - p_n )
24
25    Parameters
26    ----------
27    periods: list of floats
28        Periods in units of days
29     errors (optional): list of floats
30        Errors on periods in units of days
31    Returns
32    ----------
33    observed_spacings, observed_spacings_errors: tuple of lists of floats
34        period spacing series (delta P values) and its errors (if supplied) in units of seconds
35    """
36    spacings = []
37    if errors is None:
38        spacings_errors = None
39        for n, period_n in enumerate(periods[:-1]):
40            spacings.append(abs(period_n - periods[n + 1]) * 86400.0)
41    else:
42        spacings_errors = []
43        for n, period_n in enumerate(periods[:-1]):
44            spacings.append(abs(period_n - periods[n + 1]) * 86400.0)
45            spacings_errors.append(np.sqrt(errors[n] ** 2 + errors[n + 1] ** 2) * 86400.0)
46
47    return spacings, spacings_errors

Generate the period spacing series (delta P = p_(n+1) - p_n )

Parameters
  • periods (list of floats): Periods in units of days errors (optional): list of floats Errors on periods in units of days
Returns
  • observed_spacings, observed_spacings_errors (tuple of lists of floats): period spacing series (delta P values) and its errors (if supplied) in units of seconds
def construct_theoretical_puls_pattern( pulsation_grid_file, observations_file, method_build_series, pattern_starting_pulsation=[], which_observable='period', output_file='theoretical_frequency_patterns.hdf', asymptotic_object=None, estimated_rotation=None, grid_parameters=None, nr_cpu=None):
 51def construct_theoretical_puls_pattern(
 52    pulsation_grid_file,
 53    observations_file,
 54    method_build_series,
 55    pattern_starting_pulsation=[],
 56    which_observable="period",
 57    output_file="theoretical_frequency_patterns.hdf",
 58    asymptotic_object=None,
 59    estimated_rotation=None,
 60    grid_parameters=None,
 61    nr_cpu=None,
 62):
 63    """
 64    Construct the theoretical frequency pattern for each model in the grid, which correspond to the observed pattern.
 65    (Each theoretical model is a row in 'pulsation_grid_file'.)
 66    The rotation rate will be scaled and optimised for each theoretical pattern individually.
 67    This optimisation will not be performed if asymptotic_object=None.
 68
 69    Parameters
 70    ----------
 71    pulsation_grid_file: string
 72        path to file containing input parameters of the models, and the pulsation frequencies of those models
 73        (as generated by function 'extract_frequency_grid' in 'functions_for_gyre').
 74    observations_file: string
 75        Path to the tsv file with observations, with a column for each observable and each set of errors.
 76        Column names specify the observable, and "_err" suffix denotes that it's the error.
 77    method_build_series: string
 78        way to generate the theoretical frequency pattern from each model to match the observed pattern. Options are:
 79            provided-pulsation: build pattern from the provided pulsation    (function 'puls_series_from_given_puls')
 80            highest-frequency: build pattern from the observed highest frequency    (function 'puls_series_from_given_puls')
 81            chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations (function 'chisq_longest_sequence')
 82    pattern_starting_pulsation: list of floats
 83        Only needed if you set method_build_series=provided-pulsation
 84        Value of the pulsation to start building the pattern from, one for each separated part of the pattern.
 85        The unit of this value needs to be the same as the observable set through which_observable.
 86    which_observable: string
 87        Observable used in the theoretical pattern construction, options are 'frequency' or 'period'.
 88    output_file: string
 89        Name (can include a path) for the file containing all the pulsation frequencies of the grid.
 90    asymptotic_object: asymptotic (see 'gmode_rotation_scaling')
 91        Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
 92    estimated_rotation: float
 93        Estimation of the rotation rate of the star, used as initial value in the optimisation problem.
 94    grid_parameters: list of string
 95        List of the parameters in the theoretical grid.
 96    nr_cpu: int
 97        Number of worker processes to use in multiprocessing. The default 'None' will use the number returned by os.cpu_count().
 98    """
 99    # Read in the files with observed and theoretical frequencies as pandas DataFrames
100    obs_dataframe = pd.read_table(observations_file, sep="\s+", header=0, index_col="index")
101    theory_dataframe = pd.read_hdf(pulsation_grid_file)
102
103    obs = np.asarray(obs_dataframe[which_observable])
104    obs_err = np.asarray(obs_dataframe[f"{which_observable}_err"])
105
106    # if frequency was filled in as 0, it indicates an interruption in the pattern
107    missing_puls = np.where(obs == 0)[0]
108    # remove values indicating interruptions in the pattern
109    obs_without_missing = obs[obs != 0]
110    # remove values indicating interruptions in the pattern
111    obs_err_without_missing = obs_err[obs_err != 0]
112    # Adjust indices for removed 0-values of missing frequencies
113    missing_puls = [missing_puls[i] - i for i in range(len(missing_puls))]
114
115    # split into different parts of the interrupted pattern
116    obs_pattern_parts = np.split(obs_without_missing, missing_puls)
117    obs_err_pattern_parts = np.split(obs_err_without_missing, missing_puls)
118
119    # partial function fixes all parameters of the function except for 1 that is iterated over in the multiprocessing pool.
120    theory_pattern_func = partial(
121        theoretical_pattern_from_dfrow,
122        obs=obs,
123        obs_pattern_parts=obs_pattern_parts,
124        obs_err_pattern_parts=obs_err_pattern_parts,
125        which_observable=which_observable,
126        method_build_series=method_build_series,
127        pattern_starting_pulsation=pattern_starting_pulsation,
128        grid_parameters=grid_parameters,
129        asymptotic_object=asymptotic_object,
130        estimated_rotation=estimated_rotation,
131        plot_rotation_optimisation=False,
132    )
133
134    # Make the output file directory
135    Path(Path(output_file).parent).mkdir(parents=True, exist_ok=True)
136    # Send the rows of the dataframe iteratively to a pool of processors to get the theoretical pattern for each model
137    with multiprocessing.Pool(nr_cpu) as p:
138        freqs = p.imap(theory_pattern_func, theory_dataframe.iterrows())
139        header_parameters = ["rot", "rot_err"] + grid_parameters
140
141        for i in range(1, obs_dataframe.shape[0] + 1):
142            if i - 1 in np.where(obs_dataframe.index == "f_missing")[0]:
143                f = f"{which_observable}_missing"
144            else:
145                f = f"{which_observable}{i}"
146            header_parameters.append(f.strip())
147
148        data = []
149        for line in freqs:
150            data.append(line)
151
152    df = pd.DataFrame(data=data, columns=header_parameters)
153    df.to_hdf(path_or_buf=output_file, key="selected_puls_grid", format="table", mode="w")

Construct the theoretical frequency pattern for each model in the grid, which correspond to the observed pattern. (Each theoretical model is a row in 'pulsation_grid_file'.) The rotation rate will be scaled and optimised for each theoretical pattern individually. This optimisation will not be performed if asymptotic_object=None.

Parameters
  • pulsation_grid_file (string): path to file containing input parameters of the models, and the pulsation frequencies of those models (as generated by function 'extract_frequency_grid' in 'functions_for_gyre').
  • observations_file (string): Path to the tsv file with observations, with a column for each observable and each set of errors. Column names specify the observable, and "_err" suffix denotes that it's the error.
  • method_build_series (string): way to generate the theoretical frequency pattern from each model to match the observed pattern. Options are: provided-pulsation: build pattern from the provided pulsation (function 'puls_series_from_given_puls') highest-frequency: build pattern from the observed highest frequency (function 'puls_series_from_given_puls') chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations (function 'chisq_longest_sequence')
  • pattern_starting_pulsation (list of floats): Only needed if you set method_build_series=provided-pulsation Value of the pulsation to start building the pattern from, one for each separated part of the pattern. The unit of this value needs to be the same as the observable set through which_observable.
  • which_observable (string): Observable used in the theoretical pattern construction, options are 'frequency' or 'period'.
  • output_file (string): Name (can include a path) for the file containing all the pulsation frequencies of the grid.
  • asymptotic_object (asymptotic (see 'gmode_rotation_scaling')): Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
  • estimated_rotation (float): Estimation of the rotation rate of the star, used as initial value in the optimisation problem.
  • grid_parameters (list of string): List of the parameters in the theoretical grid.
  • nr_cpu (int): Number of worker processes to use in multiprocessing. The default 'None' will use the number returned by os.cpu_count().
def theoretical_pattern_from_dfrow( summary_grid_row, obs, obs_pattern_parts, obs_err_pattern_parts, which_observable, method_build_series, pattern_starting_pulsation=[], asymptotic_object=None, estimated_rotation=None, plot_rotation_optimisation=False, grid_parameters=None):
157def theoretical_pattern_from_dfrow(
158    summary_grid_row,
159    obs,
160    obs_pattern_parts,
161    obs_err_pattern_parts,
162    which_observable,
163    method_build_series,
164    pattern_starting_pulsation=[],
165    asymptotic_object=None,
166    estimated_rotation=None,
167    plot_rotation_optimisation=False,
168    grid_parameters=None,
169):
170    """
171    Extract model parameters and a theoretical pulsation pattern from a row of the dataFrame that contains all model parameters and pulsation frequencies.
172
173    Parameters
174    ----------
175    summary_grid_row: tuple, made of (int, pandas series)
176        tuple returned from pandas.iterrows(), first tuple entry is the row index of the pandas dataFrame
177        second tuple entry is a pandas series, containing a row from the pandas dataFrame.
178        (This row holds model parameters and pulsation frequencies.)
179    obs: numpy array, dtype=float
180        Array of observed frequencies or periods. (Ordered increasing in frequency.)
181    obs_pattern_parts : list of numpy array, dtype=float
182        Holds a numpy array (with the observed frequencies or periods) per split off part
183        of the observed pattern if it is interrupted.
184        The list contains only one array if the observed pattern is uninterrupted.
185    obs_err_pattern_parts : list of numpy array, dtype=float
186        The errors on the data in obs_pattern_parts.
187    which_observable: string
188        Which observables are used in the pattern building, options are 'frequency' or 'period'.
189    method_build_series: string
190        way to generate the theoretical frequency pattern from each model
191        to match the observed pattern. Options are:
192            provided-pulsation: build pattern from the provided pulsation    (function 'puls_series_from_given_puls')
193            highest-frequency: build pattern from the observed highest frequency    (function 'puls_series_from_given_puls')
194            chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations    (function 'chisq_longest_sequence')
195    pattern_starting_pulsation: list of floats
196        Only needed if you set method_build_series=provided-pulsation
197        Value of the pulsation to start building the pattern from, one for each separated part of the pattern.
198        The unit of this value needs to be the same as the observable set through which_observable.
199    asymptotic_object: asymptotic (see 'gmode_rotation_scaling')
200        Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
201    estimated_rotation: float
202        Estimation of the rotation rate of the star, used as initial value in the optimisation problem.
203    grid_parameters: list of string
204        List of the parameters in the theoretical grid.
205
206    Returns
207    ----------
208    list_out: list of float
209        The input parameters and pulsation frequencies of the theoretical pattern
210        (or periods, depending on 'which_observable').
211    """
212    # all keys containing n_pg (these are all the radial orders)
213    freqs = np.asarray(summary_grid_row[1].filter(like="n_pg"))
214    # array with radial orders
215    orders = np.asarray([int(o.replace("n_pg", "")) for o in summary_grid_row[1].filter(like="n_pg").index])
216    orders = orders[~np.isnan(freqs)]
217    # remove all entries that are NaN in the numpy array (for when the models have a different amount of computed modes)
218    freqs = freqs[~np.isnan(freqs)]
219
220    # Check if pattern_starting_pulsation has enough entries to not truncate other parts in the zip function.
221    if len(obs_pattern_parts) != len(pattern_starting_pulsation):
222        if method_build_series == "provided-pulsation":
223            sys.exit(
224                logger.error(
225                    "Amount of pulsations specified to build patterns from is not equal to the amount of split-off parts in the pattern."
226                )
227            )
228        # Content of pattern_starting_pulsation doesn't matter if it's not used to build the pattern.
229        else:
230            # We only care about the length if the method doesn't use specified pulsations.
231            pattern_starting_pulsation = [None] * len(obs_pattern_parts)
232
233    # In this case, rescaling nor optimisation will happen
234    if asymptotic_object is None:
235        residual = rescale_rotation_and_select_theoretical_pattern(
236            None,
237            asymptotic_object,
238            estimated_rotation,
239            freqs,
240            orders,
241            obs,
242            obs_pattern_parts,
243            obs_err_pattern_parts,
244            which_observable,
245            method_build_series,
246            pattern_starting_pulsation,
247        )
248
249        list_out = [estimated_rotation, 0]
250        for parameter in grid_parameters:
251            list_out.append(summary_grid_row[1][parameter])
252
253        selected_pulsations = obs + residual
254        list_out.extend(selected_pulsations)
255
256    else:
257        # Optimise the rotation rate and get the pulsations at that rotation rate
258        params = Parameters()
259        params.add("rotation", value=estimated_rotation, min=1e-5)
260        # Fit rotation to observed pattern with the default leastsq algorithm
261        optimise_rotation = Minimizer(
262            rescale_rotation_and_select_theoretical_pattern,
263            params,
264            fcn_args=(
265                asymptotic_object,
266                estimated_rotation,
267                freqs,
268                orders,
269                obs,
270                obs_pattern_parts,
271                obs_err_pattern_parts,
272                which_observable,
273                method_build_series,
274                pattern_starting_pulsation,
275            ),
276        )
277
278        result_minimizer = optimise_rotation.minimize()
279
280        # Search from second initial value, which is as separated from the first solution as the first initial guess.
281        search_second_initial_value = 2 * result_minimizer.params["rotation"].value - estimated_rotation
282        params = Parameters()
283        params.add("rotation", value=search_second_initial_value, min=1e-5)
284        optimise_rotation = Minimizer(
285            rescale_rotation_and_select_theoretical_pattern,
286            params,
287            fcn_args=(
288                asymptotic_object,
289                estimated_rotation,
290                freqs,
291                orders,
292                obs,
293                obs_pattern_parts,
294                obs_err_pattern_parts,
295                which_observable,
296                method_build_series,
297                pattern_starting_pulsation,
298            ),
299        )
300
301        result_minimizer2 = optimise_rotation.minimize()
302        # Select the minimizer with the initial guess that resulted in the best fit.
303        if result_minimizer2.chisqr < result_minimizer.chisqr:
304            result_minimizer = result_minimizer2
305
306        optimised_pulsations = result_minimizer.residual + obs
307
308        if result_minimizer.message != "Fit succeeded.":
309            logger.warning(
310                f"""Fitting rotation did not succeed: {result_minimizer.message}
311                            for model {summary_grid_row[1].drop(summary_grid_row[1].filter(regex='n_pg').index).drop('rot').to_dict()} using method: {method_build_series}
312                            rotation found: {result_minimizer.params['rotation'].value} with error: {result_minimizer.params['rotation'].stderr}"""
313            )
314
315        if plot_rotation_optimisation:
316            fig1, ax1 = plt.subplots()
317            if which_observable == "frequency":
318                obsperiod = 1 / obs
319                optimised_periods = 1 / optimised_pulsations
320            else:
321                obsperiod = obs
322                optimised_periods = optimised_pulsations
323
324            # Recombine observational errors in one array
325            combined_obs_err = []
326            for obs_err_part in obs_err_pattern_parts:
327                if len(combined_obs_err) > 0:
328                    combined_obs_err.extend([0])
329                combined_obs_err.extend(obs_err_part)
330            combined_obs_err = np.asarray(combined_obs_err)
331
332            spacings = generate_spacing_series(obsperiod, combined_obs_err)
333            ax1.errorbar(obsperiod[:-1], spacings[0], fmt="o", yerr=spacings[1], label="obs", color="blue", alpha=0.8)
334            ax1.plot(obsperiod[:-1], spacings[0], color="blue")
335            ax1.plot(
336                optimised_periods[:-1],
337                generate_spacing_series(optimised_periods)[0],
338                "*",
339                ls="solid",
340                color="orange",
341                label="optimised",
342            )
343            ax1.plot(
344                1 / freqs[:-1], generate_spacing_series(1 / freqs)[0], ".", ls="solid", label="initial", color="green"
345            )
346
347            fig2, ax2 = plt.subplots()
348
349            ax2.errorbar(obs, obs, fmt="o", xerr=combined_obs_err, label="obs", color="blue", alpha=0.8)
350            ax2.plot(optimised_periods, optimised_periods, "*", color="orange", label="optimised")
351            ax2.plot(1 / freqs, 1 / freqs, ".", label="initial", color="green")
352
353            ax1.legend(prop={"size": 14})
354            ax2.legend(prop={"size": 14})
355            ax1.set_title(
356                f"initial omega = {estimated_rotation}, optimised omega = {result_minimizer.params['rotation'].value}"
357            )
358            ax2.set_title(
359                f"initial omega = {estimated_rotation}, optimised omega = {result_minimizer.params['rotation'].value}"
360            )
361            plt.show()
362
363        # Create list with rotation, its error, all the input parameters, and the optimised pulsations
364        list_out = [result_minimizer.params["rotation"].value, result_minimizer.params["rotation"].stderr]
365        for parameter in grid_parameters:
366            list_out.append(summary_grid_row[1][parameter])
367        list_out.extend(optimised_pulsations)
368
369    return list_out

Extract model parameters and a theoretical pulsation pattern from a row of the dataFrame that contains all model parameters and pulsation frequencies.

Parameters
  • summary_grid_row (tuple, made of (int, pandas series)): tuple returned from pandas.iterrows(), first tuple entry is the row index of the pandas dataFrame second tuple entry is a pandas series, containing a row from the pandas dataFrame. (This row holds model parameters and pulsation frequencies.)
  • obs (numpy array, dtype=float): Array of observed frequencies or periods. (Ordered increasing in frequency.)
  • obs_pattern_parts (list of numpy array, dtype=float): Holds a numpy array (with the observed frequencies or periods) per split off part of the observed pattern if it is interrupted. The list contains only one array if the observed pattern is uninterrupted.
  • obs_err_pattern_parts (list of numpy array, dtype=float): The errors on the data in obs_pattern_parts.
  • which_observable (string): Which observables are used in the pattern building, options are 'frequency' or 'period'.
  • method_build_series (string): way to generate the theoretical frequency pattern from each model to match the observed pattern. Options are: provided-pulsation: build pattern from the provided pulsation (function 'puls_series_from_given_puls') highest-frequency: build pattern from the observed highest frequency (function 'puls_series_from_given_puls') chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations (function 'chisq_longest_sequence')
  • pattern_starting_pulsation (list of floats): Only needed if you set method_build_series=provided-pulsation Value of the pulsation to start building the pattern from, one for each separated part of the pattern. The unit of this value needs to be the same as the observable set through which_observable.
  • asymptotic_object (asymptotic (see 'gmode_rotation_scaling')): Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
  • estimated_rotation (float): Estimation of the rotation rate of the star, used as initial value in the optimisation problem.
  • grid_parameters (list of string): List of the parameters in the theoretical grid.
Returns
  • list_out (list of float): The input parameters and pulsation frequencies of the theoretical pattern (or periods, depending on 'which_observable').
def rescale_rotation_and_select_theoretical_pattern( params, asymptotic_object, estimated_rotation, freqs_input, orders, obs, obs_pattern_parts, obs_err_pattern_parts, which_observable, method_build_series, pattern_starting_pulsation):
373def rescale_rotation_and_select_theoretical_pattern(
374    params,
375    asymptotic_object,
376    estimated_rotation,
377    freqs_input,
378    orders,
379    obs,
380    obs_pattern_parts,
381    obs_err_pattern_parts,
382    which_observable,
383    method_build_series,
384    pattern_starting_pulsation,
385):
386    """
387    If rotation is not optimised in the modelling, asymptotic_object should be set to 'None' and
388    this function will just select a theoretical pulsation pattern based on the specified method.
389    If asymptotic_object is supplied, the rotation will be optimised and this will be the
390    objective function for the lmfit Minimizer class to minimise the output of.
391    Rescales the theoretical pulsation pattern using the asymptotic object from
392    'gmode_rotation_scaling', and selects a theoretical pulsation pattern based
393    on the specified method.
394
395    Parameters
396    ----------
397    params: Parameter (object of lmfit)
398        The parameter used to optimise the fit. In this case rotation in units of 1/day.
399    asymptotic_object: asymptotic (see 'gmode_rotation_scaling')
400        Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
401    estimated_rotation: float
402        Estimated rotation rate in units of 1/day. Used as initial value in the optimisation problem.
403    freqs_input: numpy array, dtype=float
404        Array of the frequencies as computed by GYRE, to be scaled to the optimal rotation rate.
405    orders: numpy array, dtype=int
406        Array with the radial orders of the theoretical input frequencies.
407    obs: numpy array, dtype=float
408        Array of observed frequencies or periods. (Ordered increasing in frequency.)
409    obs_pattern_parts: list of numpy array, dtype=float
410        Holds a numpy array with observed frequencies or periods
411        per split off part of the observed pattern if it is interrupted.
412        The list contains only one array if the observed pattern is uninterrupted.
413    obs_err_pattern_parts: list of numpy array, dtype=float
414        The errors on obs_pattern_parts
415    which_observable: string
416        Which observables are used in the pattern building, options are 'frequency' or 'period'.
417    method_build_series: string
418        way to generate the theoretical frequency pattern from each model
419        to match the observed pattern. Options are:
420            provided-pulsation: build pattern from the provided pulsation    (function 'puls_series_from_given_puls')
421            highest-frequency: build pattern from the observed highest frequency    (function 'puls_series_from_given_puls')
422            chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations    (function 'chisq_longest_sequence')
423    pattern_starting_pulsation: list of floats
424        Only needed if you set method_build_series=provided-pulsation
425        Value of the pulsation to start building the pattern from, one for each separated part of the pattern.
426        The unit of this value needs to be the same as the observable set through which_observable.
427
428    Returns
429    ----------
430    (output_pulsations - obs): numpy array, dtype=float
431        Differences between the scaled pulsations and the observations.
432        The array to be minimised by the lmfit Minimizer if rotation is optimised.
433    """
434    if asymptotic_object is not None:
435        v = params.valuesdict()
436        # To avoid division by zero in scale_pattern
437        if estimated_rotation == 0:
438            estimated_rotation = 1e-99
439        freqs = asymptotic_object.scale_pattern(freqs_input / u.d, estimated_rotation / u.d, v["rotation"] / u.d) * u.d
440        # convert astropy quantities back to floats
441        freqs = np.asarray(freqs, dtype=np.float64)
442    else:
443        freqs = freqs_input
444
445    periods = 1 / freqs
446
447    output_pulsations = []
448    for obs_part, obs_err_part, pattern_starting_pulsation_part in zip(
449        obs_pattern_parts, obs_err_pattern_parts, pattern_starting_pulsation
450    ):
451        # To indicate interruptions in the pattern
452        if len(output_pulsations) > 0:
453            output_pulsations.append(0)
454
455        if which_observable == "frequency":
456            # remove frequencies that were already chosen in a different, split-off part of the pattern
457            if len(output_pulsations) > 0:
458                # If input is in increasing radial order (decreasing n_pg, since n_pg is negative for g-modes)
459                if orders[1] == orders[0] - 1:
460                    # index -2 to get lowest, non-zero freq
461                    np.delete(freqs, np.where(freqs >= output_pulsations[-2]))
462                # If input is in decreasing radial order
463                else:
464                    np.delete(freqs, np.where(freqs <= max(output_pulsations)))
465            theory_value = freqs
466            obs_period = 1 / obs_part
467            obs_period_err = obs_err_part / obs_part**2
468            highest_obs_freq = max(obs_part)
469
470        elif which_observable == "period":
471            # remove periods that were already chosen in a different, split-off part of the pattern
472            if len(output_pulsations) > 0:
473                # If input is in increasing radial order (decreasing n_pg, since n_pg is negative for g-modes)
474                if orders[1] == orders[0] - 1:
475                    np.delete(periods, np.where(periods <= max(output_pulsations)))
476                # If input is in decreasing radial order
477                else:
478                    # index -2 to get lowest, non-zero period
479                    np.delete(periods, np.where(periods >= output_pulsations[-2]))
480            theory_value = periods
481            obs_period = obs_part
482            obs_period_err = obs_err_part
483            # highest frequency is lowest period
484            highest_obs_freq = min(obs_part)
485        else:
486            sys.exit(logger.error("Unknown observable to fit"))
487
488        if method_build_series == "provided-pulsation":
489            selected_theoretical_pulsations = puls_series_from_given_puls(
490                theory_value, obs_part, pattern_starting_pulsation_part
491            )
492        elif method_build_series == "highest-frequency":
493            selected_theoretical_pulsations = puls_series_from_given_puls(theory_value, obs_part, highest_obs_freq)
494        elif method_build_series == "chisq-longest-sequence":
495            series_chi2, final_theoretical_periods, corresponding_orders = chisq_longest_sequence(
496                periods, orders, obs_period, obs_period_err
497            )
498            if which_observable == "frequency":
499                selected_theoretical_pulsations = 1 / np.asarray(final_theoretical_periods)
500            elif which_observable == "period":
501                selected_theoretical_pulsations = final_theoretical_periods
502        else:
503            sys.exit(logger.error(f"Unrecognised method to build pulsation series: {method_build_series}"))
504
505        output_pulsations.extend(selected_theoretical_pulsations)
506
507    return output_pulsations - obs

If rotation is not optimised in the modelling, asymptotic_object should be set to 'None' and this function will just select a theoretical pulsation pattern based on the specified method. If asymptotic_object is supplied, the rotation will be optimised and this will be the objective function for the lmfit Minimizer class to minimise the output of. Rescales the theoretical pulsation pattern using the asymptotic object from 'gmode_rotation_scaling', and selects a theoretical pulsation pattern based on the specified method.

Parameters
  • params (Parameter (object of lmfit)): The parameter used to optimise the fit. In this case rotation in units of 1/day.
  • asymptotic_object (asymptotic (see 'gmode_rotation_scaling')): Object to calculate g-mode period spacing patterns in the asymptotic regime using the TAR.
  • estimated_rotation (float): Estimated rotation rate in units of 1/day. Used as initial value in the optimisation problem.
  • freqs_input (numpy array, dtype=float): Array of the frequencies as computed by GYRE, to be scaled to the optimal rotation rate.
  • orders (numpy array, dtype=int): Array with the radial orders of the theoretical input frequencies.
  • obs (numpy array, dtype=float): Array of observed frequencies or periods. (Ordered increasing in frequency.)
  • obs_pattern_parts (list of numpy array, dtype=float): Holds a numpy array with observed frequencies or periods per split off part of the observed pattern if it is interrupted. The list contains only one array if the observed pattern is uninterrupted.
  • obs_err_pattern_parts (list of numpy array, dtype=float): The errors on obs_pattern_parts
  • which_observable (string): Which observables are used in the pattern building, options are 'frequency' or 'period'.
  • method_build_series (string): way to generate the theoretical frequency pattern from each model to match the observed pattern. Options are: provided-pulsation: build pattern from the provided pulsation (function 'puls_series_from_given_puls') highest-frequency: build pattern from the observed highest frequency (function 'puls_series_from_given_puls') chisq-longest-sequence: build pattern based on longest, best matching sequence of pulsations (function 'chisq_longest_sequence')
  • pattern_starting_pulsation (list of floats): Only needed if you set method_build_series=provided-pulsation Value of the pulsation to start building the pattern from, one for each separated part of the pattern. The unit of this value needs to be the same as the observable set through which_observable.
Returns
  • (output_pulsations - obs) (numpy array, dtype=float): Differences between the scaled pulsations and the observations. The array to be minimised by the lmfit Minimizer if rotation is optimised.
def puls_series_from_given_puls(theory_in, obs, obs_to_build_from, plot=False):
511def puls_series_from_given_puls(theory_in, obs, obs_to_build_from, plot=False):
512    """
513    Generate a theoretical pulsation pattern (can be in frequency or period) from the given observations.
514    Build consecutively in radial order, starting from the theoretical value closest to the provided observational value.
515
516    Parameters
517    ----------
518    theory_in: numpy array, dtype=float
519        Array of theoretical frequencies or periods.
520    obs: numpy array, dtype=float
521        Array of observed frequencies or periods.
522    obs_to_build_from: float
523        observed frequency or period value to start building the pattern from.
524    plot: boolean
525        Make a period spacing diagram for the constructed series.
526
527    Returns
528    ----------
529    theory_sequence: list of float
530        The constructed theoretical frequency pattern
531    """
532    # get index of observation to build the series from
533    nth_obs = np.where(obs == obs_to_build_from)[0][0]
534    # search theoretical freq closest to the given observed one
535    diff = abs(theory_in - obs_to_build_from)
536    # get index of this theoretical frequency
537    index = np.where(diff == min(diff))[0][0]
538
539    # Insert a value of -1 if observations miss a theoretical counterpart in the beginning
540    theory_sequence = []
541    if (index - nth_obs) < 0:
542        for i in range(abs((index - nth_obs))):
543            theory_sequence.append(-1)
544        theory_sequence.extend(theory_in[0 : index + (len(obs) - nth_obs)])
545    else:
546        theory_sequence.extend(theory_in[index - nth_obs : index + (len(obs) - nth_obs)])
547
548    # Insert a value of -1 if observations miss a theoretical counterpart at the end
549    if index + (len(obs) - nth_obs) > len(theory_in):
550        for i in range((index + (len(obs) - nth_obs)) - len(theory_in)):
551            theory_sequence.append(-1)
552
553    if plot is True:
554        fig = plt.figure()
555        ax = fig.add_subplot(111)
556        theory = np.asarray(theory_sequence)
557        ax.plot((1 / obs)[::-1][:-1], np.diff((1 / obs)[::-1]) * 86400, "ko", lw=1.5, linestyle="-")
558        ax.plot(
559            (1.0 / theory)[::-1][:-1],
560            -np.diff(1.0 / theory)[::-1] * 86400,
561            "ko",
562            color="blue",
563            lw=1.5,
564            linestyle="--",
565            markersize=6,
566            markeredgewidth=0.0,
567        )
568        plt.show()
569
570    return theory_sequence

Generate a theoretical pulsation pattern (can be in frequency or period) from the given observations. Build consecutively in radial order, starting from the theoretical value closest to the provided observational value.

Parameters
  • theory_in (numpy array, dtype=float): Array of theoretical frequencies or periods.
  • obs (numpy array, dtype=float): Array of observed frequencies or periods.
  • obs_to_build_from (float): observed frequency or period value to start building the pattern from.
  • plot (boolean): Make a period spacing diagram for the constructed series.
Returns
  • theory_sequence (list of float): The constructed theoretical frequency pattern
def chisq_longest_sequence(theory_periods, orders, obs_periods, obs_periods_errors):
574def chisq_longest_sequence(theory_periods, orders, obs_periods, obs_periods_errors):
575    """
576    Method to extract the theoretical pattern that best matches the observed one.
577    Match each observed mode period to its best matching theoretical counterpart,
578    and adopt the longest sequence of consecutive modes found this way.
579    In case of multiple mode series with the same length, a final pattern selection
580    is made based on the best (chi-square) match between theory and observations.
581
582    Parameters
583    ----------
584    theory_periods : numpy array, dtype=float
585        Theoretical periods and their radial orders.
586    orders : numpy array, dtype=int
587        Radial orders corresponding to the periods in theory_periods.
588    obs_periods: numpy array, dtype=float
589        Observational periods.
590    obs_periods_errors: numpy array, dtype=float
591        The errors on obs_periods.
592
593    Returns
594    ----------
595    series_chi2: float
596        chi2 value of the selected theoretical frequencies.
597    final_theoretical_periods: numpy array, dtype=float
598        The selected theoretical periods that best match the observed pattern.
599    corresponding_orders: list of integers
600        The radial orders of the returned theoretical periods.
601    """
602    if len(theory_periods) < len(obs_periods):
603        return 1e16, [-1.0 for i in range(len(obs_periods))], [-1 for i in range(len(obs_periods))]
604    else:
605        # Find the best matches per observed period
606        pairs_orders = []
607        for ii, period in enumerate(obs_periods):
608            ## Chi_squared array definition
609            chi_sqrs = np.array(
610                [((period - theory_period) / obs_periods_errors[ii]) ** 2 for theory_period in theory_periods]
611            )
612
613            ## Locate the theoretical frequency (and accompanying order) with the best chi2
614            min_ind = np.where(chi_sqrs == min(chi_sqrs))[0]
615            best_match = theory_periods[min_ind][0]
616            best_order = orders[min_ind][0]
617
618            ## Toss everything together for bookkeeping
619            pairs_orders.append([period, best_match, int(best_order), chi_sqrs[min_ind][0]])
620
621        pairs_orders = np.array(pairs_orders)
622
623        # If input is in increasing radial order (decreasing n_pg, since n_pg is negative for g-modes)
624        if orders[1] == orders[0] - 1:
625            increase_or_decrease = -1
626        # If input is in decreasing radial order
627        else:
628            increase_or_decrease = 1
629
630        sequences = []
631        ## Go through all pairs of obs and theoretical frequencies and
632        ## check if the next observed frequency has a corresponding theoretical frequency
633        ## with the consecutive radial order.
634        current = []
635        lp = len(pairs_orders[:-1])
636        for ii, sett in enumerate(pairs_orders[:-1]):
637            if abs(sett[2]) == abs(pairs_orders[ii + 1][2]) + increase_or_decrease:
638                current.append(sett)
639            # If not consecutive radial order, save the current sequence and start a new one.
640            else:
641                current.append(sett)
642                sequences.append(np.array(current).reshape(len(current), 4))
643                current = []
644            if ii == lp - 1:
645                current.append(sett)
646                sequences.append(np.array(current).reshape(len(current), 4))
647                current = []
648        len_list = np.array([len(x) for x in sequences])
649        longest = np.where(len_list == max(len_list))[0]
650
651        ## Test if there really is one longest sequence
652        if len(longest) == 1:
653            lseq = sequences[longest[0]]
654
655        ## if not, pick, of all the sequences with the same length, the best based on chi2
656        else:
657            scores = [np.sum(sequences[ii][:, -1]) / len(sequences[ii]) for ii in longest]
658            min_score = np.where(scores == min(scores))[0][0]
659            lseq = sequences[longest[min_score]]
660
661        obs_ordering_ind = np.where(obs_periods == lseq[:, 0][0])[0][0]
662        thr_ordering_ind = np.where(theory_periods == lseq[:, 1][0])[0][0]
663
664        ordered_theoretical_periods = []
665        corresponding_orders = []
666
667        thr_ind_start = thr_ordering_ind - obs_ordering_ind
668        thr_ind_current = thr_ind_start
669
670        for i, oper in enumerate(obs_periods):
671            thr_ind_current = thr_ind_start + i
672            if thr_ind_current < 0:
673                tper = -1
674                ordr = -1
675            elif thr_ind_current >= len(theory_periods):
676                tper = -1
677                ordr = -1
678            else:
679                tper = theory_periods[thr_ind_current]
680                ordr = orders[thr_ind_current]
681            ordered_theoretical_periods.append(tper)
682            corresponding_orders.append(ordr)
683
684        final_theoretical_periods = np.array(ordered_theoretical_periods)
685
686        obs_series, obs_series_errors = generate_spacing_series(obs_periods, obs_periods_errors)
687        thr_series, _ = generate_spacing_series(final_theoretical_periods)
688
689        obs_series = np.array(obs_series)
690        obs_series_errors = np.array(obs_series_errors)
691        thr_series = np.array(thr_series)
692
693        series_chi2 = np.sum(((obs_series - thr_series) / obs_series_errors) ** 2) / len(obs_series)
694
695        return series_chi2, final_theoretical_periods, corresponding_orders

Method to extract the theoretical pattern that best matches the observed one. Match each observed mode period to its best matching theoretical counterpart, and adopt the longest sequence of consecutive modes found this way. In case of multiple mode series with the same length, a final pattern selection is made based on the best (chi-square) match between theory and observations.

Parameters
  • theory_periods (numpy array, dtype=float): Theoretical periods and their radial orders.
  • orders (numpy array, dtype=int): Radial orders corresponding to the periods in theory_periods.
  • obs_periods (numpy array, dtype=float): Observational periods.
  • obs_periods_errors (numpy array, dtype=float): The errors on obs_periods.
Returns
  • series_chi2 (float): chi2 value of the selected theoretical frequencies.
  • final_theoretical_periods (numpy array, dtype=float): The selected theoretical periods that best match the observed pattern.
  • corresponding_orders (list of integers): The radial orders of the returned theoretical periods.