"""Simulation properties for the population class.
This class contains the simulation properties, e.g. flow, steps and `max_time`.
"""
__authors__ = [
"Kyle Akira Rocha <kylerocha2024@u.northwestern.edu>",
"Konstantinos Kovlakas <Konstantinos.Kovlakas@unige.ch>",
"Jeffrey Andrews <jeffrey.andrews@northwestern.edu>",
"Simone Bavera <Simone.Bavera@unige.ch>",
"Nam Tran <tranhn03@gmail.com>",
]
import time
from posydon.utils.constants import age_of_universe
[docs]
class SimulationProperties:
"""Class describing the properties of a population synthesis simulation."""
def __init__(self, properties=None, **kwargs):
"""Construct the simulation properties object.
Parameters
----------
properties : object
Simulation Properties class containing, e.g. flow, steps.
extra_hooks : list of tuples
Each tuple contains a hooks class and kwargs or the extra step name
('extra_pre_evolve', 'extra_pre_step', 'extra_post_step',
'extra_post_evolve') and the corresponding function.
"""
self.kwargs = kwargs.copy()
# Check if binary_properties is passed
self.default_hooks = EvolveHooks()
self.all_hooks_classes = [self.default_hooks]
for item in kwargs.get('extra_hooks', []):
if isinstance(item, tuple):
if isinstance(item[0], type) and isinstance(item[1], dict):
cls, params = item
self.all_hooks_classes.append(cls(**params))
elif isinstance(item[0], str):
# setting extra_pre/post_step/evolve methods
setattr(self, item[0], item[1])
else:
raise ValueError(
"`extra_hooks` must be list of tuples with either "
"(i) a class deriving from EvolveHooks and a kwargs dict, "
"or (ii) the name of the extra function and the callable.")
# Binary parameters and parameterizations
self.initial_rotation = 0.0
self.mass_transfer_efficiency = 1.0
self.common_envelope_efficiency = 1.0
# Limits on simulation
if not hasattr(self, 'max_simulation_time'):
self.max_simulation_time = age_of_universe
if not hasattr(self, 'end_events'):
self.end_events = []
if not hasattr(self, 'end_states'):
self.end_states = []
# for debugging purposes
if not hasattr(self, 'max_n_steps_per_binary'):
self.max_n_steps_per_binary = 100
#if not hasattr(self, "verbose_binary_errors"):
# self.verbose_binary_errors = False
# Set functions for evolution
for key, val in kwargs.items():
if "step" not in key: # skip loading steps
setattr(self, key, val)
self.steps_loaded = False
[docs]
def load_steps(self, verbose=False):
"""Instantiate step classes and set as instance attributes.
Parameters
----------
verbose: bool
Print extra information.
Returns
-------
None
"""
if verbose:
print('STEP NAME'.ljust(20) + 'STEP FUNCTION'.ljust(25) + 'KWARGS')
for name, tup in self.kwargs.items():
if isinstance(tup, tuple):
if verbose:
print(name, tup, end='\n')
step_func, kwargs = tup
setattr(self, name, step_func(**kwargs))
self.steps_loaded = True
[docs]
def close(self):
"""Close hdf5 files before exiting."""
from posydon.binary_evol.MESA.step_mesa import MesaGridStep
from posydon.binary_evol.DT.step_detached import detached_step
all_step_funcs = [getattr(self, key) for key, val in
self.__dict__.items() if 'step_' in key]
for step_func in all_step_funcs:
if isinstance(step_func, MesaGridStep):
step_func.close()
elif isinstance(step_func, detached_step):
for grid_interpolator in [step_func.grid_Hrich, step_func.grid_strippedHe]:
grid_interpolator.close()
[docs]
def pre_evolve(self, binary):
"""Functions called before a binary evolves.
Uses all extra hooks classes or extra functions.
Parameters
----------
binary : instance of <class, BinaryStar>
The binary before evolution starts.
Returns
-------
binary : instance of <class, BinaryStar>
"""
for hooks in self.all_hooks_classes:
hooks.pre_evolve(binary)
if hasattr(self, 'extra_pre_evolve'):
self.extra_pre_evolve(binary)
return binary
[docs]
def pre_step(self, binary, step_name):
"""Prepare for step.
The method is called before every evolution step; uses all extra hooks
classes or extra functions (except for undefined next step errors).
Parameters
----------
binary : instance of <class, BinaryStar>
The binary before evolution starts.
step_name : str
The name of the step about to be called (as defined in the flow).
Returns
-------
binary : instance of <class, BinaryStar>
"""
for hooks in self.all_hooks_classes:
hooks.pre_step(binary, step_name)
if hasattr(self, 'extra_pre_step'):
self.extra_pre_step(binary, step_name)
return binary
[docs]
def post_step(self, binary, step_name):
"""Finalize step.
The method is called after every evolution step; uses all extra hooks
classes or extra functions (except for undefined next step errors).
Parameters
----------
binary : instance of <class, BinaryStar>
The binary before evolution starts.
step_name : str
The name of the step about to be called (as defined in the flow).
Returns
-------
binary : instance of <class, BinaryStar>
"""
for hooks in self.all_hooks_classes:
hooks.post_step(binary, step_name)
if hasattr(self, 'extra_post_step'):
self.extra_post_step(binary, step_name)
return binary
[docs]
def post_evolve(self, binary):
"""Finalize the evolution of the binary.
The method is called after a binary exits the evolution loop.
Uses all extra hooks classes or extra functions.
Parameters
----------
binary : instance of <class, BinaryStar>
The binary after evolution is ended.
Returns
-------
binary : instance of <class, BinaryStar>
"""
for hooks in self.all_hooks_classes:
hooks.post_evolve(binary)
if hasattr(self, 'extra_post_evolve'):
self.extra_post_evolve(binary)
return binary
[docs]
class EvolveHooks:
"""Base class for hooking into binary evolution."""
def __init__(self):
"""
Add any new output columns to the hooks constructor.
Example for extra binary columns:
self.extra_binary_col_names = ["column_name_1", "column_name_2"]
Example for extra star columns:
self.extra_star_col_names = ["column_name_1", "column_name_2"]
"""
pass
[docs]
def pre_evolve(self, binary):
"""Perform actions before a binary evolves."""
return binary
[docs]
def pre_step(self, binary, step_name):
"""Perform actions before every evolution step."""
return binary
[docs]
def post_step(self, binary, step_name):
"""Perform acctions after every evolution step."""
return binary
[docs]
def post_evolve(self, binary):
"""Perform actions after a binary exits the evolution loop."""
return binary
[docs]
class TimingHooks(EvolveHooks):
"""Add history column 'step_times' (time taken by step) to each binary.
Example
-------
>>> pop.to_df(extra_columns={'step_times': float})
"""
def __init__(self):
self.extra_binary_col_names = ["step_times"]
[docs]
def pre_evolve(self, binary):
"""Initialize the step time to match history."""
if not hasattr(binary, 'step_times'):
binary.step_times = [0.0]
return binary
[docs]
def pre_step(self, binary, step_name):
"""Record the wall time before taking the step."""
self.step_start_time = time.time()
return binary
[docs]
def post_step(self, binary, step_name):
"""Record the duration of the step."""
binary.step_times.append(time.time() - self.step_start_time)
if len(binary.event_history) > len(binary.step_times):
diff = len(binary.event_history) - len(binary.step_times)
binary.step_times += [None] * (diff)
elif len(binary.event_history) < len(binary.step_times):
last_items = len(binary.event_history)
binary.step_times = binary.step_times[-(last_items - 1):]
return binary
[docs]
def post_evolve(self, binary):
"""Add None's to step_times to match history rows."""
if binary.event == 'END' or binary.event == 'FAILED':
diff = int(len(binary.event_history) - len(binary.step_times))
binary.step_times += [None] * diff
return binary
[docs]
class StepNamesHooks(EvolveHooks):
"""Add history column 'step_name' to each binary.
Name of evolutionary step as defined in SimulationProperties.
>>> pop.to_df(extra_columns={'step_names': str})
"""
def __init__(self):
self.extra_binary_col_names = ["step_names"]
[docs]
def pre_evolve(self, binary):
"""Initialize the step name to match history."""
if not hasattr(binary, 'step_names'):
binary.step_names = ['initial_cond']
return binary
[docs]
def pre_step(self, binary, step_name):
"""Do not do anything before the step."""
return binary
[docs]
def post_step(self, binary, step_name):
"""Record the step name."""
binary.step_names.append(step_name)
len_binary_hist = len(binary.event_history)
len_step_names = len(binary.step_names)
diff = len_binary_hist - len_step_names
if len_binary_hist > len_step_names:
binary.step_names += [None] * (diff)
elif len_binary_hist < len_step_names:
binary.step_names = binary.step_names[-(len_binary_hist - 1):]
return binary
[docs]
def post_evolve(self, binary):
"""Ensure None's are append to step_names to match rows in history."""
if binary.event == 'END' or binary.event == 'FAILED':
diff = int(len(binary.event_history) - len(binary.step_names))
binary.step_names += [None]*diff
return binary
[docs]
class PrintStepInfoHooks(EvolveHooks):
"""Simple example for adding extra print info."""
[docs]
def pre_step(self, binary, step_name):
"""Print the step name for each binary, before taking it."""
print(binary.index, step_name)
return binary
[docs]
def post_evolve(self, binary):
"""Report at the end of the evolution of each binary."""
print("End evol for binary {}".format(binary.index), end='\n'*2)
return binary