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
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
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().
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').
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.
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
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.