Source code for posydon.grids.termination_flags

"""Module for computing termination flags of MESA runs.

There are four flags:
    Flag 1: Indicates why the run terminated.
    Flag 2: Denotes the mass transfer phase(s) the system has gone through.
    Flag 3: Marks the evolutionary stage of the primary star.
    Flag 4: Marks the evolutionary stage of the seconary star.

The funtion `get_flags_from_MESA_run` is responsible for getting all flags for
a run, using the more specialized functions defined in the module.
"""


__authors__ = [
    "Konstantinos Kovlakas <Konstantinos.Kovlakas@unige.ch>",
    "Ying Qin <<yingqin2013@hotmail.com>",
    "Devina Misra <devina.misra@unige.ch>",
    "Jeffrey Andrews <jeffrey.andrews@northwestern.edu>",
    "Emmanouil Zapartas <ezapartas@gmail.com>",
    "Matthias Kruckow <Matthias.Kruckow@unige.ch>",
]


import os
import gzip
import numpy as np

from posydon.utils.common_functions import (
    infer_star_state, cumulative_mass_transfer_flag, infer_mass_transfer_case
)
from posydon.utils.limits_thresholds import (
    RL_RELATIVE_OVERFLOW_THRESHOLD, LG_MTRANSFER_RATE_THRESHOLD,
    MIN_COUNT_INITIAL_RLO_BOUNDARY
)
from posydon.visualization.combine_TF import (
    TF1_POOL_STABLE, TF1_POOL_UNSTABLE, TF1_POOL_INITIAL_RLO, TF1_POOL_ERROR,
    TF2_POOL_NO_RLO, TF2_POOL_INITIAL_RLO
)
from posydon.utils.posydonwarning import Pwarn


# variables needed for inferring star states
STAR_HISTORY_VARIABLES = ["surface_h1", "center_h1", "center_he4",
                          "center_c12", "log_LH", "log_LHe", "log_Lnuc"]


[docs] def get_flag_from_MESA_output(MESA_log_path): """Return a flag about evolutionary outcome based on MESA output file. Parameters ---------- MESA_log_path : string The output file of MESA. Returns ------- str The termination flag inferred from the last line containing the strings `min_timestep_limit`, or `termination code:`, or `Terminate:`. """ termination_code = '' if MESA_log_path is not None and os.path.isfile(MESA_log_path): if MESA_log_path.endswith(".gz"): with gzip.open(MESA_log_path, "rt", errors='ignore') as log_file: log_lines = log_file.readlines() else: with open(MESA_log_path, "r", errors='ignore') as log_file: log_lines = log_file.readlines() for line in reversed(log_lines): has_term_code = line.startswith("termination code: ") has_terminate = line.startswith("Terminate: ") reached_tpagb = "Reached TPAGB" in line min_timestep = "min_timestep_limit" in line if not (has_term_code or has_terminate): if min_timestep and reached_tpagb: return "Reached TPAGB" else: truncate_at = len( "termination code: " if has_term_code else "Terminate: ") if min_timestep: # in case of "min_timestep_limit" allow to find another # termination code to overrule "min_timestep_limit" termination_code = line[truncate_at:].strip() else: return line[truncate_at:].strip() if len(termination_code)>0: return termination_code return "reach cluster timelimit"
[docs] def get_mass_transfer_flag(binary_history, history1, history2, start_at_RLO=False, mesa_flag=None): """Read flag from MESA history. In case of contact during MS, RLOF1, RLOF2 or no_RLOF. Parameters ---------- binary_history : np.array The binary history file of MESA. Can also be the history of star 1 or 2 if binary history columns. history1 : np.array The history file of MESA for star1. history2 : np.array The history file of MESA for star2. Returns ------- flag_system_evolution_history : string Possible flags are: "None", "initial_RLOF", "contact_during_MS", "no_RLOF", a cumulative MT flag, e.g, "case_A1/B1/A2" where the index indicates the donor star. """ if mesa_flag in TF1_POOL_ERROR: return "None" if mesa_flag in TF1_POOL_INITIAL_RLO: return "initial_RLOF" rel1 = binary_history["rl_relative_overflow_1"] rel2 = binary_history["rl_relative_overflow_2"] rate = binary_history["lg_mtransfer_rate"] # warning: all changes in the following lines need to be aligned with the # function posydon.utils.common_functions.infer_mass_transfer_case where_rl_rel_1 = rel1 > RL_RELATIVE_OVERFLOW_THRESHOLD where_rl_rel_2 = rel2 > RL_RELATIVE_OVERFLOW_THRESHOLD where_rl_rel_1_dominates = rel1 >= rel2 where_rl_rel_2_dominates = rel1 < rel2 if np.any(where_rl_rel_1 & where_rl_rel_2): return "contact_during_MS" where_transfer = rate > LG_MTRANSFER_RATE_THRESHOLD where_rlof_1 = (where_rl_rel_1 | where_transfer) & where_rl_rel_1_dominates where_rlof_2 = (where_rl_rel_2 | where_transfer) & where_rl_rel_2_dominates if not np.any(where_rlof_1) and not np.any(where_rlof_2): return "no_RLOF" MT = np.array([None]*len(where_rlof_1)) if np.any(where_rlof_1): star_history = history1 star_mass = binary_history["star_1_mass"] where_rlof = where_rlof_1 rel_overflow = rel1 indices_with_rlo = np.arange(len(where_rlof))[where_rlof] if not start_at_RLO and indices_with_rlo[0] == 0: return "initial_RLOF" mass_transfer_cases = [] for index in indices_with_rlo: star_state = check_state_from_history( history=star_history, mass=star_mass, model_index=index) mt_case = infer_mass_transfer_case( rl_relative_overflow=rel_overflow[index], lg_mtransfer_rate=rate[index], donor_state=star_state) mass_transfer_cases.append(mt_case) MT[where_rlof] = mass_transfer_cases if np.any(where_rlof_2): star_history = history2 star_mass = binary_history["star_2_mass"] where_rlof = where_rlof_2 rel_overflow = rel2 indices_with_rlo = np.arange(len(where_rlof))[where_rlof] if not start_at_RLO and indices_with_rlo[0] == 0: return "initial_RLOF" mass_transfer_cases = [] for index in indices_with_rlo: star_state = check_state_from_history( history=star_history, mass=star_mass, model_index=index) mt_case = infer_mass_transfer_case( rl_relative_overflow=rel_overflow[index], lg_mtransfer_rate=rate[index], donor_state=star_state) mass_transfer_cases.append(mt_case) MT[where_rlof] = [t+10 for t in mass_transfer_cases] # shift by 10 flag = cumulative_mass_transfer_flag([t for t in MT if t is not None], shift_cases=True) return flag
[docs] def check_state_from_history(history, mass, model_index=-1): """Return the final state of the star from the star history data. Parameters ---------- history: np.array MESA history of the star model_index: int Index of the model in history for which the state will be computed. By default it is the end of the evolution (last model). Returns ------- state : str The state of the star. """ if history is None: return infer_star_state(star_mass=mass[model_index], star_CO=True) for col in STAR_HISTORY_VARIABLES: if col not in history.dtype.names: raise KeyError("The data column {} is not in the ".format(col)+\ "history file. It is needed for the determination"+\ " of the star state.") variables_to_pass = {varname: history[varname][model_index] for varname in STAR_HISTORY_VARIABLES} return infer_star_state(star_CO=False, **variables_to_pass)
[docs] def get_flags_from_MESA_run(MESA_log_path, binary_history=None, history1=None, history2=None, start_at_RLO=False, newTF1=''): """Return the four termination flags. Parameters ---------- MESA_log_path: str path to the MESA terminal output binary_history, history1, history2: np.array MESA output histories. newTF1: str replacement for the termination flag from the MESA output Returns ------- flag_MESAout, flag_mass_transfer, final_state_1, final_state_2 : str flag_MESAout: actual line of MESA terminal output_path flag_mass_transfer: describes the mass transfer (e.g., case A, case B). final_state_1, final_state_2: describe the final evolutionary state of the two stars. None if history star is not provided. """ if newTF1=='': flag_out = get_flag_from_MESA_output(MESA_log_path) else: flag_out = newTF1 final_state_1 = check_state_from_history(history1, binary_history["star_1_mass"]) final_state_2 = check_state_from_history(history2, binary_history["star_2_mass"]) flag_mass_transfer = get_mass_transfer_flag(binary_history, history1, history2, start_at_RLO=start_at_RLO, mesa_flag=flag_out) return flag_out, flag_mass_transfer, final_state_1, final_state_2
[docs] def infer_interpolation_class(tf1, tf2): """Use the first two termination flags to infer the interpolation class.""" if ((tf1 in TF1_POOL_INITIAL_RLO) or (tf2 in TF2_POOL_INITIAL_RLO)): return "initial_MT" if tf1 in TF1_POOL_ERROR: return "not_converged" if tf2 in TF2_POOL_NO_RLO: return "no_MT" if tf1 in TF1_POOL_STABLE: if 'case' in tf2 and '1' in tf2 and '2' in tf2: return "stable_reverse_MT" else: return "stable_MT" if tf1 in TF1_POOL_UNSTABLE: return "unstable_MT" return "unknown"
[docs] def get_detected_initial_RLO(grid): """Generates a list of already detected initial RLO Parameters ---------- grid : a PSyGrid The grid to check. Retruns ------- list A list containing systems already detected to be initial_MT based on termination_flag_1. For each system there is a dictionary with the impartant data, e.g. initial masses, periods, termination_flags """ #new list detected = [] #go through grid N_runs = len(grid.initial_values) for i in range(N_runs): flag1 = grid.final_values[i]["termination_flag_1"] #find systems with termination because of initial overflow if flag1 == "Terminate because of overflowing initial model": mass1 = grid.initial_values[i]["star_1_mass"] mass2 = grid.initial_values[i]["star_2_mass"] period = grid.initial_values[i]["period_days"] exists_already = False #check for already existing entries of same mass combination for j, d in enumerate(detected): if (abs(d["star_1_mass"]-mass1)<1.0e-5 and abs(d["star_2_mass"]-mass2)<1.0e-5): exists_already = True #update values if new one has a larger period if d["period_days"]<period: detected[j]=({"star_1_mass": mass1, "star_2_mass": mass2, "period_days": period, "termination_flag_3": grid.final_values[i]["termination_flag_3"], "termination_flag_4": grid.final_values[i]["termination_flag_4"], }) #add masses, period, and termination flags 3 and 4 of detected # system to the list if not exists_already: detected.append({"star_1_mass": mass1, "star_2_mass": mass2, "period_days": period, "termination_flag_3": grid.final_values[i]["termination_flag_3"], "termination_flag_4": grid.final_values[i]["termination_flag_4"], }) return detected
[docs] def get_nearest_known_initial_RLO(mass1, mass2, known_initial_RLO): """Find the nearest system of initial RLO in the known ones Parameters ---------- mass1 : float star_1_mass of the run to check. mass2 : float star_2_mass of the run to check. known_initial_RLO : list of dict Boundary to apply. Retruns ------- dict Containing the key parameters (e.g. initial masses, period) of the nearest initial RLO run. """ #default values d2min = 1.0e+99 nearest = {"star_1_mass": 0.0, "star_2_mass": 0.0, "period_days": 0.0, } if len(known_initial_RLO)<MIN_COUNT_INITIAL_RLO_BOUNDARY: Pwarn("Don't apply initial RLO boundary because of too few data points" " in there.","InappropriateValueWarning") return nearest for sys in known_initial_RLO: #search for a known system with closest mass combination #use distance^2=(delta mass1)^2+(delta mass2)^2 d2 = (mass1-sys["star_1_mass"])**2 + (mass2-sys["star_2_mass"])**2 if d2<d2min: #update nearest system d2min = d2 nearest = sys return nearest