From 9e18552a2bc8e929ee13421b92a08c5ac92e6cbc Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 23 Jan 2026 15:48:28 +0100 Subject: [PATCH 01/13] first pass to only do the HMS-HMS grid setup --- bin/posydon-setup-grid | 2128 ++++++++++---------- posydon/CLI/grids/__init__.py | 0 posydon/CLI/grids/setup.py | 1766 ++++++++++++++++ posydon/unit_tests/CLI/grids/__init__.py | 1 + posydon/unit_tests/CLI/grids/test_setup.py | 542 +++++ 5 files changed, 3325 insertions(+), 1112 deletions(-) create mode 100644 posydon/CLI/grids/__init__.py create mode 100644 posydon/CLI/grids/setup.py create mode 100644 posydon/unit_tests/CLI/grids/__init__.py create mode 100644 posydon/unit_tests/CLI/grids/test_setup.py diff --git a/bin/posydon-setup-grid b/bin/posydon-setup-grid index f6978f52d7..dfdcfe7524 100755 --- a/bin/posydon-setup-grid +++ b/bin/posydon-setup-grid @@ -11,6 +11,20 @@ import subprocess import pandas from posydon.active_learning.psy_cris.utils import parse_inifile +from posydon.CLI.grids.setup import ( + construct_command_line, + generate_submission_scripts, + get_additional_user_settings, + read_grid_file, + resolve_columns, + resolve_extras, + resolve_inlists, + setup_grid_run_folder, + setup_inlist_repository, + setup_MESA_defaults, + setup_POSYDON, + setup_user, +) from posydon.grids.psygrid import PSyGrid from posydon.utils import configfile from posydon.utils import gridutils as utils @@ -53,928 +67,892 @@ def parse_commandline(): return args -def find_inlist_from_scenario(source, gitcommit, system_type): - """Dynamically find the inlists the user wants to from the supplied info - - Parameters - ---------- - - source: - - gitcommit: - - system_type: - """ - # note the directory we are in now - where_am_i_now = os.getcwd() - print("We are going to dynamically fetch the posydon inlists based on your scenario") - if source == 'posydon': - print("You have selected posydon as your source") - print("checking if we have already cloned POSYDON-MESA-INLISTS for you") - if not os.path.isdir('{0}/.posydon_mesa_inlists'.format(os.environ['HOME'])): - print("We are clonining the repo for you") - # Determine location of executables - proc = subprocess.Popen(['git', 'clone', 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git', '{0}/.posydon_mesa_inlists'.format(os.environ['HOME'])], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - (clone, err) = proc.communicate() - else: - Pwarn("git repository is already there, using that", - "OverwriteWarning") - - inlists_dir = '{0}/.posydon_mesa_inlists'.format(os.environ['HOME']) - branch = gitcommit.split('-')[0] - githash = gitcommit.split('-')[1] - - elif source == 'user': - print("You have selected user as your source " - "checking if we have already cloned USER-MESA-INLISTS for you " - "Validating the name of the git hash you want to use..." - "must be of format 'branch-githash'") - - if len(gitcommit.split('-')) != 2: - raise ValueError("You have supplied an invalid user gitcommit format, must be of format 'branch-githash'") - - branch = gitcommit.split('-')[0] - githash = gitcommit.split('-')[1] - - if not os.path.isdir('{0}/.user_mesa_inlists'.format(os.environ['HOME'])): - print("We are clonining the repo for you") - # Determine location of executables - proc = subprocess.Popen(['git', 'clone', 'https://github.com/POSYDON-code/USER-MESA-INLISTS.git', '{0}/.user_mesa_inlists'.format(os.environ['HOME'])], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - (clone, err) = proc.communicate() - else: - Pwarn("git repository is already there, using that", - "OverwriteWarning") - - inlists_dir = '{0}/.user_mesa_inlists'.format(os.environ['HOME']) - branch = gitcommit.split('-')[0] - githash = gitcommit.split('-')[1] - - else: - raise ValueError("supplied source is not valid/understood. Valid sources are user and posydon") - - os.chdir(inlists_dir) - print("checking out branch: {0}".format(branch)) - - proc = subprocess.Popen(['git', 'checkout', '{0}'.format(branch)], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - proc.wait() - - print("For posterity we are pulling (specifically needed if you already have the repo clone)") - proc = subprocess.call(['git', 'pull'], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - - - print("checking out commit/tag: {0}".format(githash)) - - proc = subprocess.Popen(['git', 'checkout', '{0}'.format(githash)], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - proc.wait() - - # if this is looking at posydon defaults, all posydon defaults build from default common inlists - if source == 'posydon': - inlists_location_common = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', "default_common_inlists") - print("Based on system_type {0} " - "We are populating the posydon inlists in the following directory: " - "{1}".format(system_type, inlists_location_common)) - - inlist1 = os.path.join(inlists_location_common, 'binary', 'inlist1') - if os.path.isfile(inlist1): - with open(inlist1) as f: - # check if we also need to find the location of the zams.data file - for line in f.readlines(): - if 'zams_filename' in line: - print("ZAMS_FILENAME detected, setting mesa_inlists['zams_filename'] for star 1") - zams_filename_1 = os.path.split(line.split("'")[1])[1] - zams_file_path = os.path.join(inlists_dir, 'r11701', "ZAMS_models", zams_filename_1) - if os.path.isfile(zams_file_path): - print("Verified locations of ZAMS data file, {0}".format(zams_file_path)) - mesa_inlists['zams_filename_1'] = "{0}".format(zams_file_path) - print("Running Single Grid: Setting mesa_star1_extras to {0}/binary/src/run_star_extras.f".format(inlists_location_common)) - - inlist2 = os.path.join(inlists_location_common, 'binary', 'inlist2') - if os.path.isfile(inlist2): - with open(inlist2) as f: - # check if we also need to find the location of the zams.data file - for line in f.readlines(): - if 'zams_filename' in line: - print("ZAMS_FILENAME detected, setting mesa_inlists['zams_filename'] for star 2") - zams_filename_2 = os.path.split(line.split("'")[1])[1] - zams_file_path = os.path.join(inlists_dir, 'r11701', "ZAMS_models", zams_filename_2) - if os.path.isfile(zams_file_path): - print("Verified locations of ZAMS data file, {0}".format(zams_file_path)) - mesa_inlists['zams_filename_2'] = "{0}".format(zams_file_path) - print("Running Single Grid: Setting mesa_star2_extras to {0}/binary/src/run_star_extras.f".format(inlists_location_common)) - - - print("Updating inifile values") +# def find_inlist_from_scenario(source, gitcommit, system_type): +# """Dynamically find the inlists the user wants to from the supplied info + +# Parameters +# ---------- + +# source: + +# gitcommit: + +# system_type: +# """ +# # note the directory we are in now +# where_am_i_now = os.getcwd() +# print("We are going to dynamically fetch the posydon inlists based on your scenario") +# if source == 'posydon': +# print("You have selected posydon as your source") +# print("checking if we have already cloned POSYDON-MESA-INLISTS for you") +# if not os.path.isdir('{0}/.posydon_mesa_inlists'.format(os.environ['HOME'])): +# print("We are clonining the repo for you") +# # Determine location of executables +# proc = subprocess.Popen(['git', 'clone', 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git', '{0}/.posydon_mesa_inlists'.format(os.environ['HOME'])], +# stdin = subprocess.PIPE, +# stdout = subprocess.PIPE, +# stderr = subprocess.PIPE +# ) +# (clone, err) = proc.communicate() +# else: +# Pwarn("git repository is already there, using that", +# "OverwriteWarning") + +# inlists_dir = '{0}/.posydon_mesa_inlists'.format(os.environ['HOME']) +# branch = gitcommit.split('-')[0] +# githash = gitcommit.split('-')[1] + +# elif source == 'user': +# print("You have selected user as your source " +# "checking if we have already cloned USER-MESA-INLISTS for you " +# "Validating the name of the git hash you want to use..." +# "must be of format 'branch-githash'") + +# if len(gitcommit.split('-')) != 2: +# raise ValueError("You have supplied an invalid user gitcommit format, must be of format 'branch-githash'") + +# branch = gitcommit.split('-')[0] +# githash = gitcommit.split('-')[1] + +# if not os.path.isdir('{0}/.user_mesa_inlists'.format(os.environ['HOME'])): +# print("We are clonining the repo for you") +# # Determine location of executables +# proc = subprocess.Popen(['git', 'clone', 'https://github.com/POSYDON-code/USER-MESA-INLISTS.git', '{0}/.user_mesa_inlists'.format(os.environ['HOME'])], +# stdin = subprocess.PIPE, +# stdout = subprocess.PIPE, +# stderr = subprocess.PIPE +# ) +# (clone, err) = proc.communicate() +# else: +# Pwarn("git repository is already there, using that", +# "OverwriteWarning") + +# inlists_dir = '{0}/.user_mesa_inlists'.format(os.environ['HOME']) +# branch = gitcommit.split('-')[0] +# githash = gitcommit.split('-')[1] + +# else: +# raise ValueError("supplied source is not valid/understood. Valid sources are user and posydon") + +# os.chdir(inlists_dir) +# print("checking out branch: {0}".format(branch)) + +# proc = subprocess.Popen(['git', 'checkout', '{0}'.format(branch)], +# stdin = subprocess.PIPE, +# stdout = subprocess.PIPE, +# stderr = subprocess.PIPE +# ) +# proc.wait() + +# print("For posterity we are pulling (specifically needed if you already have the repo clone)") +# proc = subprocess.call(['git', 'pull'], +# stdin = subprocess.PIPE, +# stdout = subprocess.PIPE, +# stderr = subprocess.PIPE +# ) + + +# print("checking out commit/tag: {0}".format(githash)) + +# proc = subprocess.Popen(['git', 'checkout', '{0}'.format(githash)], +# stdin = subprocess.PIPE, +# stdout = subprocess.PIPE, +# stderr = subprocess.PIPE +# ) +# proc.wait() + +# # if this is looking at posydon defaults, all posydon defaults build from default common inlists +# if source == 'posydon': +# inlists_location_common = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', "default_common_inlists") +# print("Based on system_type {0} " +# "We are populating the posydon inlists in the following directory: " +# "{1}".format(system_type, inlists_location_common)) + +# inlist1 = os.path.join(inlists_location_common, 'binary', 'inlist1') + +# # Max comment: This code also allows you to set the zams_filenames in the loaded inlists. +# # This is not useful to define it in multiple places. It's better to define it only once in the configuration file. +# if os.path.isfile(inlist1): +# # with open(inlist1) as f: +# # # check if we also need to find the location of the zams.data file +# # for line in f.readlines(): +# # if 'zams_filename' in line: +# # print("ZAMS_FILENAME detected, setting mesa_inlists['zams_filename'] for star 1") +# # zams_filename_1 = os.path.split(line.split("'")[1])[1] +# # zams_file_path = os.path.join(inlists_dir, 'r11701', "ZAMS_models", zams_filename_1) +# # if os.path.isfile(zams_file_path): +# # print("Verified locations of ZAMS data file, {0}".format(zams_file_path)) +# # mesa_inlists['zams_filename_1'] = "{0}".format(zams_file_path) +# print("Running Single Grid: Setting mesa_star1_extras to {0}/binary/src/run_star_extras.f".format(inlists_location_common)) + +# inlist2 = os.path.join(inlists_location_common, 'binary', 'inlist2') +# if os.path.isfile(inlist2): +# # with open(inlist2) as f: +# # # check if we also need to find the location of the zams.data file +# # for line in f.readlines(): +# # if 'zams_filename' in line: +# # print("ZAMS_FILENAME detected, setting mesa_inlists['zams_filename'] for star 2") +# # zams_filename_2 = os.path.split(line.split("'")[1])[1] +# # zams_file_path = os.path.join(inlists_dir, 'r11701', "ZAMS_models", zams_filename_2) +# # if os.path.isfile(zams_file_path): +# # print("Verified locations of ZAMS data file, {0}".format(zams_file_path)) +# # mesa_inlists['zams_filename_2'] = "{0}".format(zams_file_path) +# print("Running Single Grid: Setting mesa_star2_extras to {0}/binary/src/run_star_extras.f".format(inlists_location_common)) + + +# print("Updating inifile values") # binary inlists - mesa_inlists['star1_controls_posydon_defaults'] = '{0}/binary/inlist1'.format(inlists_location_common) - mesa_inlists['star1_job_posydon_defaults'] = '{0}/binary/inlist1'.format(inlists_location_common) - mesa_inlists['star2_controls_posydon_defaults'] = '{0}/binary/inlist2'.format(inlists_location_common) - mesa_inlists['star2_job_posydon_defaults'] = '{0}/binary/inlist2'.format(inlists_location_common) - mesa_inlists['binary_controls_posydon_defaults'] = '{0}/binary/inlist_project'.format(inlists_location_common) - mesa_inlists['binary_job_posydon_defaults'] = '{0}/binary/inlist_project'.format(inlists_location_common) + # mesa_inlists['star1_controls_posydon_defaults'] = '{0}/binary/inlist1'.format(inlists_location_common) + # mesa_inlists['star1_job_posydon_defaults'] = '{0}/binary/inlist1'.format(inlists_location_common) + # mesa_inlists['star2_controls_posydon_defaults'] = '{0}/binary/inlist2'.format(inlists_location_common) + # mesa_inlists['star2_job_posydon_defaults'] = '{0}/binary/inlist2'.format(inlists_location_common) + # mesa_inlists['binary_controls_posydon_defaults'] = '{0}/binary/inlist_project'.format(inlists_location_common) + # mesa_inlists['binary_job_posydon_defaults'] = '{0}/binary/inlist_project'.format(inlists_location_common) - # columns - mesa_inlists['star_history_columns'] = '{0}/history_columns.list'.format(inlists_location_common) - mesa_inlists['binary_history_columns'] = '{0}/binary_history_columns.list'.format(inlists_location_common) - mesa_inlists['profile_columns'] = '{0}/profile_columns.list'.format(inlists_location_common) + # # columns + # mesa_inlists['star_history_columns'] = '{0}/history_columns.list'.format(inlists_location_common) + # mesa_inlists['binary_history_columns'] = '{0}/binary_history_columns.list'.format(inlists_location_common) + # mesa_inlists['profile_columns'] = '{0}/profile_columns.list'.format(inlists_location_common) - # executables - mesa_extras['posydon_binary_extras'] = '{0}/binary/src/run_binary_extras.f'.format(inlists_location_common) - mesa_extras['posydon_star_binary_extras'] = '{0}/binary/src/run_star_extras.f'.format(inlists_location_common) + # # executables + # mesa_extras['posydon_binary_extras'] = '{0}/binary/src/run_binary_extras.f'.format(inlists_location_common) + # mesa_extras['posydon_star_binary_extras'] = '{0}/binary/src/run_star_extras.f'.format(inlists_location_common) - mesa_extras["mesa_star1_extras"] = '{0}/binary/src/run_star_extras.f'.format(inlists_location_common) + # mesa_extras["mesa_star1_extras"] = '{0}/binary/src/run_star_extras.f'.format(inlists_location_common) # so this is sufficient for system type HMS-HMS but not for others, for others we stack the above inlists on further inlists # we also need to see if we are looking at a folder for binaries or singles - if system_type == 'HeMS-HMS': - inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) - print("Based on system_type {0} " - "We are populating the user inlists in the following directory: " - "{1}".format(system_type, inlists_location)) - - if os.path.isfile(os.path.join(inlists_location, "binary", "inlist1")): - mesa_inlists['star1_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - mesa_inlists['star1_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - - if os.path.isfile(os.path.join(inlists_location, "binary", "inlist2")): - mesa_inlists['star2_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - mesa_inlists['star2_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - - # check for star formation parameters - if os.path.isdir(os.path.join(inlists_location, "star1_formation")): - # We are making star1 so we can unset the zams file we were going to use for star1 - print("We are making star1 so we can unset the zams file we were going to use for star1") - # print() - # print("HERE") - # print() - mesa_inlists['zams_filename_1'] = None - - # Figure out how many user star1 formation steps there are and layer the posydon default inlists on all of them - star1_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) - print("These are the user we are using to make star1: {0}".format(star1_formation_scenario)) - print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) - mesa_inlists['star1_formation_controls_posydon_defaults'] = [] - mesa_inlists['star1_formation_job_posydon_defaults'] = [] - for i in range(len(star1_formation_scenario)): - mesa_inlists['star1_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - mesa_inlists['star1_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - - mesa_inlists['star1_formation_controls_user'] = star1_formation_scenario - mesa_inlists['star1_formation_job_user'] = star1_formation_scenario - - elif system_type != "HMS-HMS" and not mesa_inlists['single_star_grid']: - inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) - print("Based on system_type {0} " - "We are populating the user inlists in the following directory: " - "{1}".format(system_type, inlists_location)) - - # determine where the binary inlist(s) are - if os.path.isfile(os.path.join(inlists_location, "binary", "inlist_project")): - mesa_inlists['binary_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist_project")) - mesa_inlists['binary_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist_project")) - - if os.path.isfile(os.path.join(inlists_location, "binary", "inlist1")): - mesa_inlists['star1_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - mesa_inlists['star1_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - - if os.path.isfile(os.path.join(inlists_location, "binary", "inlist2")): - mesa_inlists['star2_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - mesa_inlists['star2_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - - if os.path.isfile(os.path.join(inlists_location, "history_columns.list")): - mesa_inlists['star_history_columns'] = os.path.join(inlists_location, "history_columns.list") - - if os.path.isfile(os.path.join(inlists_location, "binary_history_columns.list")): - mesa_inlists['binary_history_columns'] = os.path.join(inlists_location, "binary_history_columns.list") - - if os.path.isfile(os.path.join(inlists_location, "profile_columns.list")): - mesa_inlists['profile_columns'] = os.path.join(inlists_location, "profile_columns.list") - - if os.path.isfile(os.path.join(inlists_location, "src", "run_binary_extras.f")): - mesa_extras['user_binary_extras'] = '{0}'.format(os.path.join(inlists_location, "src", "run_binary_extras.f")) - - if os.path.isfile(os.path.join(inlists_location, "src", "run_star_extras.f")): - mesa_extras['user_star_binary_extras'] = '{0}'.format(os.path.join(inlists_location, "src", "run_star_extras.f")) - - # check for star formation parameters - if os.path.isdir(os.path.join(inlists_location, "star1_formation")): - # We are making star1 so we can unset the zams file we were going to use for star1 - print("We are making star1 so we can unset the zams file we were going to use for star1") - mesa_inlists['zams_filename_1'] = None - - # Figure out how many user star1 formation steps there are and layer the posydon default inlists on all of them - star1_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) - print("These are the user we are using to make star1: {0}".format(star1_formation_scenario)) - print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) - mesa_inlists['star1_formation_controls_posydon_defaults'] = [] - mesa_inlists['star1_formation_job_posydon_defaults'] = [] - for i in range(len(star1_formation_scenario)): - mesa_inlists['star1_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - mesa_inlists['star1_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - - mesa_inlists['star1_formation_controls_user'] = star1_formation_scenario - mesa_inlists['star1_formation_job_user'] = star1_formation_scenario - - if os.path.isdir(os.path.join(inlists_location, "star2_formation")): - # We are making star2 so we can unset the zams file we were going to use for star2 - print("We are making star2 so we can unset the zams file we were going to use for star2") - mesa_inlists['zams_filename_2'] = None - - # Figure out how many user star2 formation steps there are and layer the posydon default inlists on all of them - star2_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star2_formation", "*step*"))) - print("These are the user we are using to make star2: {0}".format(star2_formation_scenario)) - print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) - mesa_inlists['star2_formation_controls_posydon_defaults'] = [] - mesa_inlists['star2_formation_job_posydon_defaults'] = [] - for i in range(len(star2_formation_scenario)): - mesa_inlists['star2_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - mesa_inlists['star2_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - - mesa_inlists['star2_formation_controls_user'] = star2_formation_scenario - mesa_inlists['star2_formation_job_user'] = star2_formation_scenario - - if system_type == "HMS-HMS" and mesa_inlists['single_star_grid']: - print("You want a single star HMS grid, this means that we need to make a user inlist on the fly with a single line " - "x_logical_ctrl(1)=.true.") - # write star1 formation step to file - special_single_star_user_inlist = os.path.join(os.getcwd(), "special_single_star_user_inlist") - if os.path.exists(special_single_star_user_inlist): - Pwarn('Replace '+special_single_star_user_inlist, - "OverwriteWarning") - with open(special_single_star_user_inlist, 'wb') as f: - f.write(b'&controls\n\n') - f.write('\t{0} = {1}\n'.format("x_logical_ctrl(1)", ".true.").encode('utf-8')) - - f.write(b'\n\n') - - f.write(b""" - / ! end of star1_controls namelist - - """) - mesa_inlists['star1_controls_special'] = special_single_star_user_inlist - elif system_type == "CO-He_star" and mesa_inlists['single_star_grid']: - inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) - print("Based on system_type {0} " - "We are populating the user inlists in the following directory: " - "{1}".format(system_type, inlists_location)) - - # We are making star2 so we can unset the zams file we were going to use for star2 - print("We are making star2 so we can unset the zams file we were going to use for star2") - mesa_inlists['zams_filename_2'] = None - - # Find the user single star controls - single_star_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) - print("These are the user inlists used in the single star grid: {0}".format(single_star_scenario)) - mesa_inlists['star1_controls_user'] = single_star_scenario - mesa_inlists['star1_job_user'] = single_star_scenario - print("You want a single star He grid, " - "this means that we need to make the inlist that will be used to evolve the system " - "and make sure we layer on the line " - "x_logical_ctrl(1)=.true.") - # write star1 formation step to file - special_single_star_user_inlist = os.path.join(os.getcwd(), "special_single_star_user_inlist") - if os.path.exists(special_single_star_user_inlist): - Pwarn('Replace '+special_single_star_user_inlist, - "OverwriteWarning") - with open(special_single_star_user_inlist, 'wb') as f: - f.write(b'&controls\n\n') - f.write('\t{0} = {1}\n'.format("x_logical_ctrl(1)", ".true.").encode('utf-8')) - - f.write(b'\n\n') - f.write(b""" - / ! end of star1_controls namelist - - """) - f.write(b'&star_job\n\n') - f.write(b""" - / / ! end of star_job namelist - - """) - mesa_inlists['star1_controls_special'].append(special_single_star_user_inlist) - - # change back to where I was - os.chdir(where_am_i_now) - - return - -def construct_static_inlist(mesa_inlists, grid_parameters, working_directory=os.getcwd()): - """Based on all the inlists that were passed construc the MESA project dir - - Parameters - mesa_inlists: - All of the values from the mesa_inlists section of the inifile (`dict`) - - grid_parameters: - A list of the parameters from the csv file so we can determine all of - the inlist parameters for binary, star1 and star2 that will be changing - with this grid - - Returns: - inlists - """ + # if system_type == 'HeMS-HMS': + # inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) + # print("Based on system_type {0} " + # "We are populating the user inlists in the following directory: " + # "{1}".format(system_type, inlists_location)) + + # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist1")): + # mesa_inlists['star1_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) + # mesa_inlists['star1_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) + + # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist2")): + # mesa_inlists['star2_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) + # mesa_inlists['star2_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) + + # # check for star formation parameters + # if os.path.isdir(os.path.join(inlists_location, "star1_formation")): + # # We are making star1 so we can unset the zams file we were going to use for star1 + # print("We are making star1 so we can unset the zams file we were going to use for star1") + # # print() + # # print("HERE") + # # print() + # mesa_inlists['zams_filename_1'] = None + + # # Figure out how many user star1 formation steps there are and layer the posydon default inlists on all of them + # star1_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) + # print("These are the user we are using to make star1: {0}".format(star1_formation_scenario)) + # print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) + # mesa_inlists['star1_formation_controls_posydon_defaults'] = [] + # mesa_inlists['star1_formation_job_posydon_defaults'] = [] + # for i in range(len(star1_formation_scenario)): + # mesa_inlists['star1_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) + # mesa_inlists['star1_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) + + # mesa_inlists['star1_formation_controls_user'] = star1_formation_scenario + # mesa_inlists['star1_formation_job_user'] = star1_formation_scenario + + # elif system_type != "HMS-HMS" and not mesa_inlists['single_star_grid']: + # inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) + # print("Based on system_type {0} " + # "We are populating the user inlists in the following directory: " + # "{1}".format(system_type, inlists_location)) + + # # determine where the binary inlist(s) are + # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist_project")): + # mesa_inlists['binary_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist_project")) + # mesa_inlists['binary_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist_project")) + + # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist1")): + # mesa_inlists['star1_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) + # mesa_inlists['star1_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) + + # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist2")): + # mesa_inlists['star2_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) + # mesa_inlists['star2_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) + + # if os.path.isfile(os.path.join(inlists_location, "history_columns.list")): + # mesa_inlists['star_history_columns'] = os.path.join(inlists_location, "history_columns.list") + + # if os.path.isfile(os.path.join(inlists_location, "binary_history_columns.list")): + # mesa_inlists['binary_history_columns'] = os.path.join(inlists_location, "binary_history_columns.list") + + # if os.path.isfile(os.path.join(inlists_location, "profile_columns.list")): + # mesa_inlists['profile_columns'] = os.path.join(inlists_location, "profile_columns.list") + + # if os.path.isfile(os.path.join(inlists_location, "src", "run_binary_extras.f")): + # mesa_extras['user_binary_extras'] = '{0}'.format(os.path.join(inlists_location, "src", "run_binary_extras.f")) + + # if os.path.isfile(os.path.join(inlists_location, "src", "run_star_extras.f")): + # mesa_extras['user_star_binary_extras'] = '{0}'.format(os.path.join(inlists_location, "src", "run_star_extras.f")) + + # # check for star formation parameters + # if os.path.isdir(os.path.join(inlists_location, "star1_formation")): + # # We are making star1 so we can unset the zams file we were going to use for star1 + # print("We are making star1 so we can unset the zams file we were going to use for star1") + # mesa_inlists['zams_filename_1'] = None + + # # Figure out how many user star1 formation steps there are and layer the posydon default inlists on all of them + # star1_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) + # print("These are the user we are using to make star1: {0}".format(star1_formation_scenario)) + # print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) + # mesa_inlists['star1_formation_controls_posydon_defaults'] = [] + # mesa_inlists['star1_formation_job_posydon_defaults'] = [] + # for i in range(len(star1_formation_scenario)): + # mesa_inlists['star1_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) + # mesa_inlists['star1_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) + + # mesa_inlists['star1_formation_controls_user'] = star1_formation_scenario + # mesa_inlists['star1_formation_job_user'] = star1_formation_scenario + + # if os.path.isdir(os.path.join(inlists_location, "star2_formation")): + # # We are making star2 so we can unset the zams file we were going to use for star2 + # print("We are making star2 so we can unset the zams file we were going to use for star2") + # mesa_inlists['zams_filename_2'] = None + + # # Figure out how many user star2 formation steps there are and layer the posydon default inlists on all of them + # star2_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star2_formation", "*step*"))) + # print("These are the user we are using to make star2: {0}".format(star2_formation_scenario)) + # print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) + # mesa_inlists['star2_formation_controls_posydon_defaults'] = [] + # mesa_inlists['star2_formation_job_posydon_defaults'] = [] + # for i in range(len(star2_formation_scenario)): + # mesa_inlists['star2_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) + # mesa_inlists['star2_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) + + # mesa_inlists['star2_formation_controls_user'] = star2_formation_scenario + # mesa_inlists['star2_formation_job_user'] = star2_formation_scenario + + # if system_type == "HMS-HMS" and mesa_inlists['single_star_grid']: + # print("You want a single star HMS grid, this means that we need to make a user inlist on the fly with a single line " + # "x_logical_ctrl(1)=.true.") + # # write star1 formation step to file + # special_single_star_user_inlist = os.path.join(os.getcwd(), "special_single_star_user_inlist") + # if os.path.exists(special_single_star_user_inlist): + # Pwarn('Replace '+special_single_star_user_inlist, + # "OverwriteWarning") + # with open(special_single_star_user_inlist, 'wb') as f: + # f.write(b'&controls\n\n') + # f.write('\t{0} = {1}\n'.format("x_logical_ctrl(1)", ".true.").encode('utf-8')) + + # f.write(b'\n\n') + + # f.write(b""" + # / ! end of star1_controls namelist + + # """) + # mesa_inlists['star1_controls_special'] = special_single_star_user_inlist + # elif system_type == "CO-He_star" and mesa_inlists['single_star_grid']: + # inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) + # print("Based on system_type {0} " + # "We are populating the user inlists in the following directory: " + # "{1}".format(system_type, inlists_location)) + + # # We are making star2 so we can unset the zams file we were going to use for star2 + # print("We are making star2 so we can unset the zams file we were going to use for star2") + # mesa_inlists['zams_filename_2'] = None + + # # Find the user single star controls + # single_star_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) + # print("These are the user inlists used in the single star grid: {0}".format(single_star_scenario)) + # mesa_inlists['star1_controls_user'] = single_star_scenario + # mesa_inlists['star1_job_user'] = single_star_scenario + # print("You want a single star He grid, " + # "this means that we need to make the inlist that will be used to evolve the system " + # "and make sure we layer on the line " + # "x_logical_ctrl(1)=.true.") + # # write star1 formation step to file + # special_single_star_user_inlist = os.path.join(os.getcwd(), "special_single_star_user_inlist") + # if os.path.exists(special_single_star_user_inlist): + # Pwarn('Replace '+special_single_star_user_inlist, + # "OverwriteWarning") + # with open(special_single_star_user_inlist, 'wb') as f: + # f.write(b'&controls\n\n') + # f.write('\t{0} = {1}\n'.format("x_logical_ctrl(1)", ".true.").encode('utf-8')) + + # f.write(b'\n\n') + # f.write(b""" + # / ! end of star1_controls namelist + + # """) + # f.write(b'&star_job\n\n') + # f.write(b""" + # / / ! end of star_job namelist + + # """) + # mesa_inlists['star1_controls_special'].append(special_single_star_user_inlist) + + # # change back to where I was + # os.chdir(where_am_i_now) + + # return + +# def construct_static_inlist(mesa_inlists, grid_parameters, working_directory=os.getcwd()): +# """Based on all the inlists that were passed construc the MESA project dir + +# Parameters +# mesa_inlists: +# All of the values from the mesa_inlists section of the inifile (`dict`) + +# grid_parameters: +# A list of the parameters from the csv file so we can determine all of +# the inlist parameters for binary, star1 and star2 that will be changing +# with this grid + +# Returns: +# inlists +# """ # To make it backwards compatible - if 'zams_filename' in mesa_inlists.keys(): - mesa_inlists['zams_filename_1'] = mesa_inlists['zams_filename'] - mesa_inlists['zams_filename_2'] = mesa_inlists['zams_filename'] + # if 'zams_filename' in mesa_inlists.keys(): + # mesa_inlists['zams_filename_1'] = mesa_inlists['zams_filename'] + # mesa_inlists['zams_filename_2'] = mesa_inlists['zams_filename'] - if 'zams_filename_1' not in mesa_inlists.keys(): - mesa_inlists['zams_filename_1'] = None + # if 'zams_filename_1' not in mesa_inlists.keys(): + # mesa_inlists['zams_filename_1'] = None - if 'zams_filename_2' not in mesa_inlists.keys(): - mesa_inlists['zams_filename_2'] = None + # if 'zams_filename_2' not in mesa_inlists.keys(): + # mesa_inlists['zams_filename_2'] = None - if 'single_star_grid' not in mesa_inlists.keys(): - mesa_inlists['single_star_grid'] = False + # if 'single_star_grid' not in mesa_inlists.keys(): + # mesa_inlists['single_star_grid'] = False ######################################## ### CONSTRUCT BINARY INLIST PARAMS ### ######################################## - # inlist project controls binary_job and binary_controls - inlist_binary_project = os.path.join(working_directory, 'binary', 'inlist_project') - # inlist1 (controls star_job1 and star_controls1 - inlist_star1_binary = os.path.join(working_directory, 'binary', 'inlist1') - # inlist1 (controls star_job2 and star_controls2 - inlist_star2_binary = os.path.join(working_directory, 'binary', 'inlist2') - - # Initialize some stuff - final_binary_controls = {} - final_binary_job = {} - final_star1_binary_job = {} - final_star2_binary_job = {} - final_star1_binary_controls = {} - final_star2_binary_controls = {} - - if not mesa_inlists['single_star_grid']: - for k, v in mesa_inlists.items(): - if v is not None: - if 'binary_controls' in k: - section = '&binary_controls' - controls_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in controls_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - final_binary_controls[k1] = v1 - - elif 'binary_job' in k: - section = '&binary_job' - job_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in job_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - final_binary_job[k1] = v1 - - elif 'star1_job' in k: - section = '&star_job' - star_job1_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in star_job1_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - final_star1_binary_job[k1] = v1 - - elif 'star2_job' in k: - section = '&star_job' - star_job2_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in star_job2_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - final_star2_binary_job[k1] = v1 - - elif 'star1_controls' in k: - section = '&controls' - star_control1_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in star_control1_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - if 'num_x_ctrls' in k1: - # This is a special default that the default value in the .defaults - # file in MESA does not work because it is a placeholder - final_star1_binary_controls[k1.replace('num_x_ctrls','1')] = v1 - else: - final_star1_binary_controls[k1] = v1 - - elif 'star2_controls' in k: - section = '&controls' - star_control2_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in star_control2_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - if 'num_x_ctrls' in k1: - # This is a special default that the default value in the .defaults - # file in MESA does not work because it is a placeholder - final_star2_binary_controls[k1.replace('num_x_ctrls','1')] = v1 - else: - final_star2_binary_controls[k1] = v1 + # # inlist project controls binary_job and binary_controls + # inlist_binary_project = os.path.join(working_directory, 'binary', 'inlist_project') + # # inlist1 (controls star_job1 and star_controls1 + # inlist_star1_binary = os.path.join(working_directory, 'binary', 'inlist1') + # # inlist1 (controls star_job2 and star_controls2 + # inlist_star2_binary = os.path.join(working_directory, 'binary', 'inlist2') + + # # Initialize some stuff + # final_binary_controls = {} + # final_binary_job = {} + # final_star1_binary_job = {} + # final_star2_binary_job = {} + # final_star1_binary_controls = {} + # final_star2_binary_controls = {} + + # if not mesa_inlists['single_star_grid']: + # for k, v in mesa_inlists.items(): + # if v is not None: + # if 'binary_controls' in k: + # section = '&binary_controls' + # controls_dict = utils.clean_inlist_file(v, section=section) + # for k1,v1 in controls_dict[section].items(): + # # remove any hidden inlists extras since that is not how we want to do things + # if ('read_extra' in k1) or ('inlist' in k1): continue + # final_binary_controls[k1] = v1 + + # elif 'binary_job' in k: + # section = '&binary_job' + # job_dict = utils.clean_inlist_file(v, section=section) + # for k1,v1 in job_dict[section].items(): + # # remove any hidden inlists extras since that is not how we want to do things + # if ('read_extra' in k1) or ('inlist' in k1): continue + # final_binary_job[k1] = v1 + + # elif 'star1_job' in k: + # section = '&star_job' + # star_job1_dict = utils.clean_inlist_file(v, section=section) + # for k1,v1 in star_job1_dict[section].items(): + # # remove any hidden inlists extras since that is not how we want to do things + # if ('read_extra' in k1) or ('inlist' in k1): continue + # final_star1_binary_job[k1] = v1 + + # elif 'star2_job' in k: + # section = '&star_job' + # star_job2_dict = utils.clean_inlist_file(v, section=section) + # for k1,v1 in star_job2_dict[section].items(): + # # remove any hidden inlists extras since that is not how we want to do things + # if ('read_extra' in k1) or ('inlist' in k1): continue + # final_star2_binary_job[k1] = v1 + + # elif 'star1_controls' in k: + # section = '&controls' + # star_control1_dict = utils.clean_inlist_file(v, section=section) + # for k1,v1 in star_control1_dict[section].items(): + # # remove any hidden inlists extras since that is not how we want to do things + # if ('read_extra' in k1) or ('inlist' in k1): continue + # if 'num_x_ctrls' in k1: + # # This is a special default that the default value in the .defaults + # # file in MESA does not work because it is a placeholder + # final_star1_binary_controls[k1.replace('num_x_ctrls','1')] = v1 + # else: + # final_star1_binary_controls[k1] = v1 + + # elif 'star2_controls' in k: + # section = '&controls' + # star_control2_dict = utils.clean_inlist_file(v, section=section) + # for k1,v1 in star_control2_dict[section].items(): + # # remove any hidden inlists extras since that is not how we want to do things + # if ('read_extra' in k1) or ('inlist' in k1): continue + # if 'num_x_ctrls' in k1: + # # This is a special default that the default value in the .defaults + # # file in MESA does not work because it is a placeholder + # final_star2_binary_controls[k1.replace('num_x_ctrls','1')] = v1 + # else: + # final_star2_binary_controls[k1] = v1 # detemine which is any of the parameters are binary_controls or binary_job params - grid_params_binary_controls = [param for param in grid_parameters if param in final_binary_controls.keys()] - print("Grid parameters that effect binary_controls: {0}".format(','.join(grid_params_binary_controls))) - grid_params_binary_job = [param for param in grid_parameters if param in final_binary_job.keys()] - print("Grid parameters that effect binary_job: {0}".format(','.join(grid_params_binary_job))) + # grid_params_binary_controls = [param for param in grid_parameters if param in final_binary_controls.keys()] + # print("Grid parameters that effect binary_controls: {0}".format(','.join(grid_params_binary_controls))) + # grid_params_binary_job = [param for param in grid_parameters if param in final_binary_job.keys()] + # print("Grid parameters that effect binary_job: {0}".format(','.join(grid_params_binary_job))) - grid_params_star1_binary_controls = [param for param in grid_parameters if param in final_star1_binary_controls.keys()] - print("Grid parameters that effect star1_binary_controls: {0}".format(','.join(grid_params_star1_binary_controls))) - grid_params_star1_binary_job = [param for param in grid_parameters if param in final_star1_binary_job.keys()] - print("Grid parameters that effect star1_binary_job: {0}".format(','.join(grid_params_star1_binary_job))) + # grid_params_star1_binary_controls = [param for param in grid_parameters if param in final_star1_binary_controls.keys()] + # print("Grid parameters that effect star1_binary_controls: {0}".format(','.join(grid_params_star1_binary_controls))) + # grid_params_star1_binary_job = [param for param in grid_parameters if param in final_star1_binary_job.keys()] + # print("Grid parameters that effect star1_binary_job: {0}".format(','.join(grid_params_star1_binary_job))) - grid_params_star2_binary_controls = [param for param in grid_parameters if param in final_star2_binary_controls.keys()] - print("Grid parameters that effect star2_binary_controls: {0}".format(','.join(grid_params_star2_binary_controls))) - grid_params_star2_binary_job = [param for param in grid_parameters if param in final_star2_binary_job.keys()] - print("Grid parameters that effect star2_binary_job: {0}".format(','.join(grid_params_star2_binary_job))) + # grid_params_star2_binary_controls = [param for param in grid_parameters if param in final_star2_binary_controls.keys()] + # print("Grid parameters that effect star2_binary_controls: {0}".format(','.join(grid_params_star2_binary_controls))) + # grid_params_star2_binary_job = [param for param in grid_parameters if param in final_star2_binary_job.keys()] + # print("Grid parameters that effect star2_binary_job: {0}".format(','.join(grid_params_star2_binary_job))) # depending on if there are any grid parameters that effect star1 or star2 we need to actually # do a read star extras step - if grid_params_star1_binary_controls: - final_star1_binary_controls['read_extra_controls_inlist1'] = '.true.' - final_star1_binary_controls['extra_controls_inlist1_name'] = "'inlist_grid_star1_binary_controls'" + # if grid_params_star1_binary_controls: + # final_star1_binary_controls['read_extra_controls_inlist1'] = '.true.' + # final_star1_binary_controls['extra_controls_inlist1_name'] = "'inlist_grid_star1_binary_controls'" - if grid_params_star2_binary_controls: - final_star2_binary_controls['read_extra_controls_inlist1'] = '.true.' - final_star2_binary_controls['extra_controls_inlist1_name'] = "'inlist_grid_star2_binary_controls'" + # if grid_params_star2_binary_controls: + # final_star2_binary_controls['read_extra_controls_inlist1'] = '.true.' + # final_star2_binary_controls['extra_controls_inlist1_name'] = "'inlist_grid_star2_binary_controls'" - if grid_params_star1_binary_job: - final_star1_binary_job['read_extra_star_job_inlist1'] = '.true.' - final_star1_binary_job['extra_star_job_inlist1_name'] = "'inlist_grid_star1_binary_job'" + # if grid_params_star1_binary_job: + # final_star1_binary_job['read_extra_star_job_inlist1'] = '.true.' + # final_star1_binary_job['extra_star_job_inlist1_name'] = "'inlist_grid_star1_binary_job'" - if grid_params_star2_binary_job: - final_star2_binary_job['read_extra_star_job_inlist1'] = '.true.' - final_star2_binary_job['extra_star_job_inlist1_name'] = "'inlist_grid_star2_binary_job'" + # if grid_params_star2_binary_job: + # final_star2_binary_job['read_extra_star_job_inlist1'] = '.true.' + # final_star2_binary_job['extra_star_job_inlist1_name'] = "'inlist_grid_star2_binary_job'" # We want to point the binary_job section to the star1 and star2 inlist we just made - final_binary_job['inlist_names(1)'] = "'{0}'".format(inlist_star1_binary) - final_binary_job['inlist_names(2)'] = "'{0}'".format(inlist_star2_binary) + #final_binary_job['inlist_names(1)'] = "'{0}'".format(inlist_star1_binary) + #final_binary_job['inlist_names(2)'] = "'{0}'".format(inlist_star2_binary) ######################## ### STAR 1 FORMATION ### ######################## # Check the number of inlists provided to the star1 formation sections # of the inifile - star1_formation = {} - - # if we have provided a pre-computed zams model, it does not matter if we wanted to form star1 and star2 for - # the binary step, we have supceded this with the zams_filename - if (mesa_inlists['zams_filename_1'] is not None) and (not mesa_inlists['single_star_grid']): - star1_formation_dictionary = {} - elif mesa_inlists['single_star_grid']: - star1_formation_dictionary = dict(filter(lambda elem: (('star1_job' in elem[0]) or ('star1_controls' in elem[0])) and elem[1] is not None, mesa_inlists.items())) - else: - # create dictionary of only these sections - star1_formation_dictionary = dict(filter(lambda elem: 'star1_formation' in elem[0] and elem[1] is not None, mesa_inlists.items())) - - # See if the user even supplied inlists for doing star1_formation - if star1_formation_dictionary: - # initialize the string argument for star1 formation that will be passed to posydon-run-grid - inlist_star1_formation = '' - # check the number of inlists in each star1 formation parameter. We will take calculate the max number and treat that as - # the number of star1 formation steps desired before making the final star1 model that will be fed into the binary exectuable - number_of_star1_formation_steps = 1 - for k, v in star1_formation_dictionary.items(): - if type(v) == list: - number_of_star1_formation_steps = max(number_of_star1_formation_steps, len(v)) - - for step in range(number_of_star1_formation_steps): - star1_formation['step{0}'.format(step)] = {} - star1_formation['step{0}'.format(step)]['inlist_file'] = os.path.join(working_directory, 'star1', 'inlist_step{0}'.format(step)) - for k, v in star1_formation_dictionary.items(): - star1_formation['step{0}'.format(step)][k] = v[step] if type(v) == list else v - - # Now we loop over each star1 formation step and construct the final star1 formation inlist for each step - for step, step_inlists in enumerate(star1_formation.values()): - # there is a new one of these final star1 formation inlists per step - final_star1_formation_controls = {} - final_star1_formation_job = {} - for k, v in step_inlists.items(): - if ('star1_formation_controls' in k) or ('star1_controls' in k): - section = '&controls' - controls_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in controls_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - if 'num_x_ctrls' in k1: - # This is a special default that the default value in the .defaults - # file in MESA does not work because it is a placeholder - final_star1_formation_controls[k1.replace('num_x_ctrls','1')] = v1 - else: - final_star1_formation_controls[k1] = v1 - - if ('star1_formation_job' in k) or ('star1_job' in k): - section = '&star_job' - controls_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in controls_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - final_star1_formation_job[k1] = v1 - - # The user supplied a way to form star1 and we need to update dictionary of parameters and their values correctly - # We want to make sure that the binary inlists load up the properly saved models from star1 formation - final_star1_binary_job['create_pre_main_sequence_model'] = ".false." - final_star1_binary_job['load_saved_model'] = ".true." - final_star1_binary_job['saved_model_name'] = "'initial_star1_step{0}.mod'".format(step) - # if this is step0 then we simply overwrite the save_model_when_terminate and - # save_model_filename parts of the inlists. However, for all steps higher than - # step 0 we need to have that step load in the model from the step - # below the current step - if step == 0: - final_star1_formation_job['save_model_when_terminate'] = '.true.' - final_star1_formation_job['save_model_filename'] = "'initial_star1_step{0}.mod'".format(step) - else: - final_star1_formation_job['create_pre_main_sequence_model'] = ".false." - final_star1_formation_job['load_saved_model'] = ".true." - final_star1_formation_job['saved_model_name'] = "'initial_star1_step{0}.mod'".format(step-1) - final_star1_formation_job['save_model_when_terminate'] = '.true.' - final_star1_formation_job['save_model_filename'] = "'initial_star1_step{0}.mod'".format(step) - - if (mesa_inlists['zams_filename_1'] is not None) and (mesa_inlists['single_star_grid']): - final_star1_formation_controls['zams_filename'] = "'{0}'".format(mesa_inlists['zams_filename_1']) - elif (mesa_inlists['zams_filename_1'] is None) and (mesa_inlists['single_star_grid']) and ('zams_filename_1' in final_star1_formation_controls.keys()): - final_star1_formation_controls.pop("zams_filename", None) - - # write star1 formation step to file - if os.path.exists(step_inlists['inlist_file']): - Pwarn('Replace '+step_inlists['inlist_file'], - "OverwriteWarning") - with open(step_inlists['inlist_file'], 'wb') as f: - f.write(b'&controls\n\n') - for k,v in final_star1_formation_controls.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b'\n\n') - - f.write(b""" - / ! end of star1_controls namelist - - """) - - f.write(b'&star_job\n\n') - for k,v in final_star1_formation_job.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b""" - / ! end of star1_job namelist - - """) - # Construct star1 formation argument string to be passed to posydon-run-grid - inlist_star1_formation += ' {0}'.format(step_inlists['inlist_file']) - else: - inlist_star1_formation = None +# star1_formation = {} + +# # if we have provided a pre-computed zams model, it does not matter if we wanted to form star1 and star2 for +# # the binary step, we have supceded this with the zams_filename +# if (mesa_inlists['zams_filename_1'] is not None) and (not mesa_inlists['single_star_grid']): +# star1_formation_dictionary = {} +# elif mesa_inlists['single_star_grid']: +# star1_formation_dictionary = dict(filter(lambda elem: (('star1_job' in elem[0]) or ('star1_controls' in elem[0])) and elem[1] is not None, mesa_inlists.items())) +# else: +# # create dictionary of only these sections +# star1_formation_dictionary = dict(filter(lambda elem: 'star1_formation' in elem[0] and elem[1] is not None, mesa_inlists.items())) + +# # See if the user even supplied inlists for doing star1_formation +# if star1_formation_dictionary: +# # initialize the string argument for star1 formation that will be passed to posydon-run-grid +# inlist_star1_formation = '' +# # check the number of inlists in each star1 formation parameter. We will take calculate the max number and treat that as +# # the number of star1 formation steps desired before making the final star1 model that will be fed into the binary exectuable +# number_of_star1_formation_steps = 1 +# for k, v in star1_formation_dictionary.items(): +# if type(v) == list: +# number_of_star1_formation_steps = max(number_of_star1_formation_steps, len(v)) + +# for step in range(number_of_star1_formation_steps): +# star1_formation['step{0}'.format(step)] = {} +# star1_formation['step{0}'.format(step)]['inlist_file'] = os.path.join(working_directory, 'star1', 'inlist_step{0}'.format(step)) +# for k, v in star1_formation_dictionary.items(): +# star1_formation['step{0}'.format(step)][k] = v[step] if type(v) == list else v + +# # Now we loop over each star1 formation step and construct the final star1 formation inlist for each step +# for step, step_inlists in enumerate(star1_formation.values()): +# # there is a new one of these final star1 formation inlists per step +# final_star1_formation_controls = {} +# final_star1_formation_job = {} +# for k, v in step_inlists.items(): +# if ('star1_formation_controls' in k) or ('star1_controls' in k): +# section = '&controls' +# controls_dict = utils.clean_inlist_file(v, section=section) +# for k1,v1 in controls_dict[section].items(): +# # remove any hidden inlists extras since that is not how we want to do things +# if ('read_extra' in k1) or ('inlist' in k1): continue +# if 'num_x_ctrls' in k1: +# # This is a special default that the default value in the .defaults +# # file in MESA does not work because it is a placeholder +# final_star1_formation_controls[k1.replace('num_x_ctrls','1')] = v1 +# else: +# final_star1_formation_controls[k1] = v1 + +# if ('star1_formation_job' in k) or ('star1_job' in k): +# section = '&star_job' +# controls_dict = utils.clean_inlist_file(v, section=section) +# for k1,v1 in controls_dict[section].items(): +# # remove any hidden inlists extras since that is not how we want to do things +# if ('read_extra' in k1) or ('inlist' in k1): continue +# final_star1_formation_job[k1] = v1 + +# # The user supplied a way to form star1 and we need to update dictionary of parameters and their values correctly +# # We want to make sure that the binary inlists load up the properly saved models from star1 formation +# final_star1_binary_job['create_pre_main_sequence_model'] = ".false." +# final_star1_binary_job['load_saved_model'] = ".true." +# final_star1_binary_job['saved_model_name'] = "'initial_star1_step{0}.mod'".format(step) +# # if this is step0 then we simply overwrite the save_model_when_terminate and +# # save_model_filename parts of the inlists. However, for all steps higher than +# # step 0 we need to have that step load in the model from the step +# # below the current step +# if step == 0: +# final_star1_formation_job['save_model_when_terminate'] = '.true.' +# final_star1_formation_job['save_model_filename'] = "'initial_star1_step{0}.mod'".format(step) +# else: +# final_star1_formation_job['create_pre_main_sequence_model'] = ".false." +# final_star1_formation_job['load_saved_model'] = ".true." +# final_star1_formation_job['saved_model_name'] = "'initial_star1_step{0}.mod'".format(step-1) +# final_star1_formation_job['save_model_when_terminate'] = '.true.' +# final_star1_formation_job['save_model_filename'] = "'initial_star1_step{0}.mod'".format(step) + +# if (mesa_inlists['zams_filename_1'] is not None) and (mesa_inlists['single_star_grid']): +# final_star1_formation_controls['zams_filename'] = "'{0}'".format(mesa_inlists['zams_filename_1']) +# elif (mesa_inlists['zams_filename_1'] is None) and (mesa_inlists['single_star_grid']) and ('zams_filename_1' in final_star1_formation_controls.keys()): +# final_star1_formation_controls.pop("zams_filename", None) + +# # write star1 formation step to file +# if os.path.exists(step_inlists['inlist_file']): +# Pwarn('Replace '+step_inlists['inlist_file'], +# "OverwriteWarning") +# with open(step_inlists['inlist_file'], 'wb') as f: +# f.write(b'&controls\n\n') +# for k,v in final_star1_formation_controls.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b'\n\n') + +# f.write(b""" +# / ! end of star1_controls namelist + +# """) + +# f.write(b'&star_job\n\n') +# for k,v in final_star1_formation_job.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b""" +# / ! end of star1_job namelist + +# """) +# # Construct star1 formation argument string to be passed to posydon-run-grid +# inlist_star1_formation += ' {0}'.format(step_inlists['inlist_file']) +# else: +# inlist_star1_formation = None + +# ######################## +# ### STAR 2 FORMATION ### +# ######################## +# # Check the number of inlists provided to the star2 formation sections +# # of the inifile +# star2_formation = {} + +# # if we have provided a pre-computed zams model, it does not matter if we wanted to form star1 and star2 for +# # the binary step, we have supceded this with the zams_filename_2 +# if mesa_inlists['zams_filename_2'] is not None: +# star2_formation_dictionary = {} +# else: +# # create dictionary of only these sections +# star2_formation_dictionary = dict(filter(lambda elem: 'star2_formation' in elem[0] and elem[1] is not None, mesa_inlists.items())) + +# # See if the user even supplied inlists for doing star2_formation +# if star2_formation_dictionary: +# # initialize the string argument for star2 formation that will be passed to posydon-run-grid +# inlist_star2_formation = '' +# # check the number of inlists in each star2 formation parameter. We will take calculate the max number and treat that as +# # the number of star2 formation steps desired before making the final star2 model that will be fed into the binary exectuable +# number_of_star2_formation_steps = 1 +# for k, v in star2_formation_dictionary.items(): +# if type(v) == list: +# number_of_star2_formation_steps = max(number_of_star2_formation_steps, len(v)) + +# for step in range(number_of_star2_formation_steps): +# star2_formation['step{0}'.format(step)] = {} +# star2_formation['step{0}'.format(step)]['inlist_file'] = os.path.join(working_directory, 'star2', 'inlist_step{0}'.format(step)) +# for k, v in star2_formation_dictionary.items(): +# star2_formation['step{0}'.format(step)][k] = v[step] if type(v) == list else v + +# # Now we loop over each star2 formation step and construct the final star2 formation inlist for each step +# for step, step_inlists in enumerate(star2_formation.values()): +# final_star2_formation_controls = {} +# final_star2_formation_job = {} +# for k, v in step_inlists.items(): +# if 'star2_formation_controls' in k: +# section = '&controls' +# controls_dict = utils.clean_inlist_file(v, section=section) +# for k1,v1 in controls_dict[section].items(): +# # remove any hidden inlists extras since that is not how we want to do things +# if ('read_extra' in k1) or ('inlist' in k1): continue +# if 'num_x_ctrls' in k1: +# # This is a special default that the default value in the .defaults +# # file in MESA does not work because it is a placeholder +# final_star2_formation_controls[k1.replace('num_x_ctrls','1')] = v1 +# else: +# final_star2_formation_controls[k1] = v1 + +# if 'star2_formation_job' in k: +# section = '&star_job' +# controls_dict = utils.clean_inlist_file(v, section=section) +# for k1,v1 in controls_dict[section].items(): +# # remove any hidden inlists extras since that is not how we want to do things +# if ('read_extra' in k1) or ('inlist' in k1): continue +# final_star2_formation_job[k1] = v1 + +# # then the user supplied a star2 formation and we need to update dictionary of parameters and their values correctly +# # We want to make sure that the binary inlists load up the properly saved models from star2 formation +# final_star2_binary_job['create_pre_main_sequence_model'] = ".false." +# final_star2_binary_job['load_saved_model'] = ".true." +# final_star2_binary_job['saved_model_name'] = "'initial_star2_step{0}.mod'".format(step) +# # if this is step0 then we simply overwrite the save_model_when_terminate and +# # save_model_filename parts of the inlists. However, for all steps higher than +# # step 0 we need to have that step load in the model from the step +# # below the current step +# if step == 0: +# final_star2_formation_job['save_model_when_terminate'] = '.true.' +# final_star2_formation_job['save_model_filename'] = "'initial_star2_step{0}.mod'".format(step) +# else: +# final_star2_formation_job['create_pre_main_sequence_model'] = ".false." +# final_star2_formation_job['load_saved_model'] = ".true." +# final_star2_formation_job['saved_model_name'] = "'initial_star2_step{0}.mod'".format(step-1) +# final_star2_formation_job['save_model_when_terminate'] = '.true.' +# final_star2_formation_job['save_model_filename'] = "'initial_star2_step{0}.mod'".format(step) + +# if os.path.exists(step_inlists['inlist_file']): +# Pwarn('Replace '+step_inlists['inlist_file'], +# "OverwriteWarning") +# with open(step_inlists['inlist_file'], 'wb') as f: +# f.write(b'&controls\n\n') +# for k,v in final_star2_formation_controls.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b'\n\n') + +# f.write(b""" +# / ! end of star2_controls namelist + +# """) + +# f.write(b'&star_job\n\n') +# for k,v in final_star2_formation_job.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b""" +# / ! end of star2_job namelist + +# """) +# # Construct star2 formation argument string to be passed to posydon-run-grid +# inlist_star2_formation += ' {0}'.format(step_inlists['inlist_file']) +# else: +# inlist_star2_formation = None + + + +# ########################################## +# ###### WRITE MESA BINARY INLISTS ####### +# ########################################## +# # now that we have all the parameters and their correct values +# # we now write our own inlist_project, inlist1 and inlist2 for the binary +# if os.path.exists(inlist_binary_project): +# Pwarn('Replace '+inlist_binary_project, "OverwriteWarning") +# with open(inlist_binary_project, 'wb') as f: +# f.write(b'&binary_controls\n\n') +# for k,v in final_binary_controls.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b'\n/ ! end of binary_controls namelist') + +# f.write(b'\n\n') + +# f.write(b'&binary_job\n\n') +# for k,v in final_binary_job.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b""" +# / ! end of binary_job namelist +# """) + +# if os.path.exists(inlist_star1_binary): +# Pwarn('Replace '+inlist_star1_binary, "OverwriteWarning") +# with open(inlist_star1_binary, 'wb') as f: +# f.write(b'&controls\n\n') +# for k,v in final_star1_binary_controls.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b'\n\n') + +# f.write(b""" +# / ! end of star1_controls namelist + +# """) + +# f.write(b'&star_job\n\n') +# for k,v in final_star1_binary_job.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b""" +# / ! end of star1_job namelist + +# """) + +# if os.path.exists(inlist_star2_binary): +# Pwarn('Replace '+inlist_star2_binary, "OverwriteWarning") +# with open(inlist_star2_binary, 'wb') as f: +# f.write(b'&controls\n\n') +# for k,v in final_star2_binary_controls.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b'\n\n') + +# f.write(b""" +# / ! end of star2_controls namelist + +# """) + +# f.write(b'&star_job\n\n') +# for k,v in final_star2_binary_job.items(): +# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) + +# f.write(b""" +# / ! end of star2_job namelist + +# """) + +# return inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, inlist_star2_binary + +# def make_executables(mesa_extras, working_directory=os.getcwd()): +# """Pass mesa extra function and compile binary executable on the fly +# """ + +# # First, make individual star executables +# star1_src_folder = os.path.join(working_directory, 'star1', 'src') +# if os.path.exists(star1_src_folder): shutil.rmtree(star1_src_folder) +# os.makedirs(star1_src_folder) + +# star1_make_folder = os.path.join(working_directory, 'star1', 'make') +# if os.path.exists(star1_make_folder): shutil.rmtree(star1_make_folder) +# os.makedirs(star1_make_folder) + +# star2_src_folder = os.path.join(working_directory, 'star2', 'src') +# if os.path.exists(star2_src_folder): shutil.rmtree(star2_src_folder) +# os.makedirs(star2_src_folder) + +# star2_make_folder = os.path.join(working_directory, 'star2', 'make') +# if os.path.exists(star2_make_folder): shutil.rmtree(star2_make_folder) +# os.makedirs(star2_make_folder) + +# # Now make the binary folder +# binary_src_folder = os.path.join(working_directory, 'binary', 'src') +# if os.path.exists(binary_src_folder): shutil.rmtree(binary_src_folder) +# os.makedirs(binary_src_folder) + +# binary_make_folder = os.path.join(working_directory, 'binary', 'make') +# if os.path.exists(binary_make_folder): shutil.rmtree(binary_make_folder) +# os.makedirs(binary_make_folder) + +# if os.path.exists('mk'): +# Pwarn('Replace mk', "OverwriteWarning") +# with open('mk', "w") as f: +# # first we need to cd into the make folder +# for k, v in mesa_extras.items(): +# if v is not None: +# if ('binary_extras' in k) or ('binary_run' in k): +# shutil.copy(v, binary_src_folder) +# elif ('star_run' in k): +# shutil.copy(v, star1_src_folder) +# shutil.copy(v, star2_src_folder) +# elif ('star1_extras' in k): +# shutil.copy(v, star1_src_folder) +# elif ('star2_extras' in k): +# shutil.copy(v, star2_src_folder) +# elif 'makefile_binary' in k: +# shutil.copy(v, os.path.join(binary_make_folder, k)) +# f.write('cd {0}\n'.format(binary_make_folder)) +# f.write('make -f {0}\n'.format(k)) +# elif 'makefile_star' in k: +# shutil.copy(v, os.path.join(star1_make_folder, k)) +# f.write('cd {0}\n'.format(star1_make_folder)) +# f.write('make -f {0}\n'.format(k)) +# shutil.copy(v, os.path.join(star2_make_folder, k)) +# f.write('cd {0}\n'.format(star2_make_folder)) +# f.write('make -f {0}\n'.format(k)) +# elif 'mesa_dir' == k: +# continue +# else: +# shutil.copy(v, working_directory) + +# os.system("chmod 755 mk") +# os.system('./mk') +# return os.path.join(working_directory,'binary','binary'), \ +# os.path.join(working_directory,'star1','star'), \ +# os.path.join(working_directory,'star2','star'), \ + +# def construct_command_line(number_of_mpi_processes, path_to_grid, +# binary_exe, star1_exe, star2_exe, +# inlist_binary_project, inlist_star1_binary, inlist_star2_binary, +# inlist_star1_formation, inlist_star2_formation, +# star_history_columns, binary_history_columns, profile_columns, +# run_directory, grid_type, path_to_run_grid_exec, +# psycris_inifile=None, keep_profiles=False, +# keep_photos=False): +# """Based on the inifile construct the command line call to posydon-run-grid +# """ +# if grid_type == "fixed": +# command_line = 'python {15} --mesa-grid {1} --mesa-binary-executable {2} ' +# elif grid_type == "dynamic": +# command_line = 'mpirun --bind-to none -np {0} python -m mpi4py {15} --mesa-grid {1} --mesa-binary-executable {2} ' +# else: +# raise ValueError("grid_type can either be fixed or dynamic not anything else") +# command_line += '--mesa-star1-executable {3} --mesa-star2-executable {4} --mesa-binary-inlist-project {5} ' +# command_line += '--mesa-binary-inlist1 {6} --mesa-binary-inlist2 {7} --mesa-star1-inlist-project {8} ' +# command_line += '--mesa-star2-inlist-project {9} --mesa-star-history-columns {10} ' +# command_line += '--mesa-binary-history-columns {11} --mesa-profile-columns {12} ' +# command_line += '--output-directory {13} --grid-type {14} ' +# command_line += '--psycris-inifile {16}' +# if keep_profiles: +# command_line += ' --keep_profiles' +# if keep_photos: +# command_line += ' --keep_photos' +# command_line = command_line.format(number_of_mpi_processes, +# path_to_grid, +# binary_exe, +# star1_exe, +# star2_exe, +# inlist_binary_project, +# inlist_star1_binary, +# inlist_star2_binary, +# inlist_star1_formation, +# inlist_star2_formation, +# star_history_columns, +# binary_history_columns, +# profile_columns, +# run_directory, +# grid_type, +# path_to_run_grid_exec, +# psycris_inifile) +# return command_line + +# Define column types and their filenames +column_types = ['star_history_columns', 'binary_history_columns', 'profile_columns'] +column_filenames = ['history_columns.list', 'binary_history_columns.list', 'profile_columns.list'] + +# Define extras keys +extras_keys = ['makefile_binary', 'makefile_star', 'binary_run' + 'star_run', 'binary_extras', 'star_binary_extras', 'star1_extras', 'star2_extras',] - ######################## - ### STAR 2 FORMATION ### - ######################## - # Check the number of inlists provided to the star2 formation sections - # of the inifile - star2_formation = {} - - # if we have provided a pre-computed zams model, it does not matter if we wanted to form star1 and star2 for - # the binary step, we have supceded this with the zams_filename_2 - if mesa_inlists['zams_filename_2'] is not None: - star2_formation_dictionary = {} - else: - # create dictionary of only these sections - star2_formation_dictionary = dict(filter(lambda elem: 'star2_formation' in elem[0] and elem[1] is not None, mesa_inlists.items())) - - # See if the user even supplied inlists for doing star2_formation - if star2_formation_dictionary: - # initialize the string argument for star2 formation that will be passed to posydon-run-grid - inlist_star2_formation = '' - # check the number of inlists in each star2 formation parameter. We will take calculate the max number and treat that as - # the number of star2 formation steps desired before making the final star2 model that will be fed into the binary exectuable - number_of_star2_formation_steps = 1 - for k, v in star2_formation_dictionary.items(): - if type(v) == list: - number_of_star2_formation_steps = max(number_of_star2_formation_steps, len(v)) - - for step in range(number_of_star2_formation_steps): - star2_formation['step{0}'.format(step)] = {} - star2_formation['step{0}'.format(step)]['inlist_file'] = os.path.join(working_directory, 'star2', 'inlist_step{0}'.format(step)) - for k, v in star2_formation_dictionary.items(): - star2_formation['step{0}'.format(step)][k] = v[step] if type(v) == list else v - - # Now we loop over each star2 formation step and construct the final star2 formation inlist for each step - for step, step_inlists in enumerate(star2_formation.values()): - final_star2_formation_controls = {} - final_star2_formation_job = {} - for k, v in step_inlists.items(): - if 'star2_formation_controls' in k: - section = '&controls' - controls_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in controls_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - if 'num_x_ctrls' in k1: - # This is a special default that the default value in the .defaults - # file in MESA does not work because it is a placeholder - final_star2_formation_controls[k1.replace('num_x_ctrls','1')] = v1 - else: - final_star2_formation_controls[k1] = v1 - - if 'star2_formation_job' in k: - section = '&star_job' - controls_dict = utils.clean_inlist_file(v, section=section) - for k1,v1 in controls_dict[section].items(): - # remove any hidden inlists extras since that is not how we want to do things - if ('read_extra' in k1) or ('inlist' in k1): continue - final_star2_formation_job[k1] = v1 - - # then the user supplied a star2 formation and we need to update dictionary of parameters and their values correctly - # We want to make sure that the binary inlists load up the properly saved models from star2 formation - final_star2_binary_job['create_pre_main_sequence_model'] = ".false." - final_star2_binary_job['load_saved_model'] = ".true." - final_star2_binary_job['saved_model_name'] = "'initial_star2_step{0}.mod'".format(step) - # if this is step0 then we simply overwrite the save_model_when_terminate and - # save_model_filename parts of the inlists. However, for all steps higher than - # step 0 we need to have that step load in the model from the step - # below the current step - if step == 0: - final_star2_formation_job['save_model_when_terminate'] = '.true.' - final_star2_formation_job['save_model_filename'] = "'initial_star2_step{0}.mod'".format(step) - else: - final_star2_formation_job['create_pre_main_sequence_model'] = ".false." - final_star2_formation_job['load_saved_model'] = ".true." - final_star2_formation_job['saved_model_name'] = "'initial_star2_step{0}.mod'".format(step-1) - final_star2_formation_job['save_model_when_terminate'] = '.true.' - final_star2_formation_job['save_model_filename'] = "'initial_star2_step{0}.mod'".format(step) - - if os.path.exists(step_inlists['inlist_file']): - Pwarn('Replace '+step_inlists['inlist_file'], - "OverwriteWarning") - with open(step_inlists['inlist_file'], 'wb') as f: - f.write(b'&controls\n\n') - for k,v in final_star2_formation_controls.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b'\n\n') - - f.write(b""" - / ! end of star2_controls namelist - - """) - - f.write(b'&star_job\n\n') - for k,v in final_star2_formation_job.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b""" - / ! end of star2_job namelist - - """) - # Construct star2 formation argument string to be passed to posydon-run-grid - inlist_star2_formation += ' {0}'.format(step_inlists['inlist_file']) - else: - inlist_star2_formation = None - - ########################################## - ###### MESA BINARY OUTPUT CONTROLS ####### - ########################################## - if mesa_inlists['final_profile_star1']: - final_star1_binary_job['write_profile_when_terminate'] = ".true." - final_star1_binary_job['filename_for_profile_when_terminate'] = "'final_profile_star1.data'" - else: - final_star1_binary_job['write_profile_when_terminate'] = ".false." - - if mesa_inlists['final_profile_star2']: - final_star2_binary_job['write_profile_when_terminate'] = ".true." - final_star2_binary_job['filename_for_profile_when_terminate'] = "'final_profile_star2.data'" - else: - final_star2_binary_job['write_profile_when_terminate'] = ".false." - - if mesa_inlists['final_model_star1']: - final_star1_binary_job['save_model_when_terminate'] = ".true." - final_star1_binary_job['save_model_filename'] = "'final_star1.mod'" - else: - final_star1_binary_job['save_model_when_terminate'] = ".false." - - if mesa_inlists['final_model_star2']: - final_star2_binary_job['save_model_when_terminate'] = ".true." - final_star2_binary_job['save_model_filename'] = "'final_star2.mod'" - else: - final_star2_binary_job['save_model_when_terminate'] = ".false." - - if mesa_inlists['history_star1']: - final_star1_binary_controls['do_history_file'] = ".true." - else: - final_star1_binary_controls['do_history_file'] = ".false." - - if mesa_inlists['history_star2']: - final_star2_binary_controls['do_history_file'] = ".true." - else: - final_star2_binary_controls['do_history_file'] = ".false." - - final_binary_controls['history_interval'] = mesa_inlists['history_interval'] - final_star1_binary_controls['history_interval'] = mesa_inlists['history_interval'] - final_star2_binary_controls['history_interval'] = mesa_inlists['history_interval'] - - if not mesa_inlists['binary_history']: - final_binary_controls['history_interval'] = "-1" - - # update the controls for star1 star2 for the binary with the precomputed zams model - if mesa_inlists['zams_filename_1'] is not None: - final_star1_binary_controls['zams_filename'] = "'{0}'".format(mesa_inlists['zams_filename_1']) - if mesa_inlists['zams_filename_2'] is not None: - final_star2_binary_controls['zams_filename'] = "'{0}'".format(mesa_inlists['zams_filename_2']) - - ########################################## - ###### WRITE MESA BINARY INLISTS ####### - ########################################## - # now that we have all the parameters and their correct values - # we now write our own inlist_project, inlist1 and inlist2 for the binary - if os.path.exists(inlist_binary_project): - Pwarn('Replace '+inlist_binary_project, "OverwriteWarning") - with open(inlist_binary_project, 'wb') as f: - f.write(b'&binary_controls\n\n') - for k,v in final_binary_controls.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b'\n/ ! end of binary_controls namelist') - - f.write(b'\n\n') - - f.write(b'&binary_job\n\n') - for k,v in final_binary_job.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b""" -/ ! end of binary_job namelist - """) - - if os.path.exists(inlist_star1_binary): - Pwarn('Replace '+inlist_star1_binary, "OverwriteWarning") - with open(inlist_star1_binary, 'wb') as f: - f.write(b'&controls\n\n') - for k,v in final_star1_binary_controls.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b'\n\n') - - f.write(b""" -/ ! end of star1_controls namelist - -""") - - f.write(b'&star_job\n\n') - for k,v in final_star1_binary_job.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b""" -/ ! end of star1_job namelist - -""") - - if os.path.exists(inlist_star2_binary): - Pwarn('Replace '+inlist_star2_binary, "OverwriteWarning") - with open(inlist_star2_binary, 'wb') as f: - f.write(b'&controls\n\n') - for k,v in final_star2_binary_controls.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b'\n\n') - - f.write(b""" -/ ! end of star2_controls namelist - -""") - - f.write(b'&star_job\n\n') - for k,v in final_star2_binary_job.items(): - f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - - f.write(b""" -/ ! end of star2_job namelist - -""") - - return inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, inlist_star2_binary - -def make_executables(mesa_extras, working_directory=os.getcwd()): - """Pass mesa extra function and compile binary executable on the fly - """ - - # First, make individual star executables - star1_src_folder = os.path.join(working_directory, 'star1', 'src') - if os.path.exists(star1_src_folder): shutil.rmtree(star1_src_folder) - os.makedirs(star1_src_folder) - - star1_make_folder = os.path.join(working_directory, 'star1', 'make') - if os.path.exists(star1_make_folder): shutil.rmtree(star1_make_folder) - os.makedirs(star1_make_folder) - - star2_src_folder = os.path.join(working_directory, 'star2', 'src') - if os.path.exists(star2_src_folder): shutil.rmtree(star2_src_folder) - os.makedirs(star2_src_folder) - - star2_make_folder = os.path.join(working_directory, 'star2', 'make') - if os.path.exists(star2_make_folder): shutil.rmtree(star2_make_folder) - os.makedirs(star2_make_folder) - - # Now make the binary folder - binary_src_folder = os.path.join(working_directory, 'binary', 'src') - if os.path.exists(binary_src_folder): shutil.rmtree(binary_src_folder) - os.makedirs(binary_src_folder) - - binary_make_folder = os.path.join(working_directory, 'binary', 'make') - if os.path.exists(binary_make_folder): shutil.rmtree(binary_make_folder) - os.makedirs(binary_make_folder) - - if os.path.exists('mk'): - Pwarn('Replace mk', "OverwriteWarning") - with open('mk', "w") as f: - # first we need to cd into the make folder - for k, v in mesa_extras.items(): - if v is not None: - if ('binary_extras' in k) or ('binary_run' in k): - shutil.copy(v, binary_src_folder) - elif ('star_run' in k): - shutil.copy(v, star1_src_folder) - shutil.copy(v, star2_src_folder) - elif ('star1_extras' in k): - shutil.copy(v, star1_src_folder) - elif ('star2_extras' in k): - shutil.copy(v, star2_src_folder) - elif 'makefile_binary' in k: - shutil.copy(v, os.path.join(binary_make_folder, k)) - f.write('cd {0}\n'.format(binary_make_folder)) - f.write('make -f {0}\n'.format(k)) - elif 'makefile_star' in k: - shutil.copy(v, os.path.join(star1_make_folder, k)) - f.write('cd {0}\n'.format(star1_make_folder)) - f.write('make -f {0}\n'.format(k)) - shutil.copy(v, os.path.join(star2_make_folder, k)) - f.write('cd {0}\n'.format(star2_make_folder)) - f.write('make -f {0}\n'.format(k)) - elif 'mesa_dir' == k: - continue - else: - shutil.copy(v, working_directory) - - os.system("chmod 755 mk") - os.system('./mk') - return os.path.join(working_directory,'binary','binary'), \ - os.path.join(working_directory,'star1','star'), \ - os.path.join(working_directory,'star2','star'), \ - -def construct_command_line(number_of_mpi_processes, path_to_grid, - binary_exe, star1_exe, star2_exe, - inlist_binary_project, inlist_star1_binary, inlist_star2_binary, - inlist_star1_formation, inlist_star2_formation, - star_history_columns, binary_history_columns, profile_columns, - run_directory, grid_type, path_to_run_grid_exec, - psycris_inifile=None, keep_profiles=False, - keep_photos=False): - """Based on the inifile construct the command line call to posydon-run-grid - """ - if grid_type == "fixed": - command_line = 'python {15} --mesa-grid {1} --mesa-binary-executable {2} ' - elif grid_type == "dynamic": - command_line = 'mpirun --bind-to none -np {0} python -m mpi4py {15} --mesa-grid {1} --mesa-binary-executable {2} ' - else: - raise ValueError("grid_type can either be fixed or dynamic not anything else") - command_line += '--mesa-star1-executable {3} --mesa-star2-executable {4} --mesa-binary-inlist-project {5} ' - command_line += '--mesa-binary-inlist1 {6} --mesa-binary-inlist2 {7} --mesa-star1-inlist-project {8} ' - command_line += '--mesa-star2-inlist-project {9} --mesa-star-history-columns {10} ' - command_line += '--mesa-binary-history-columns {11} --mesa-profile-columns {12} ' - command_line += '--output-directory {13} --grid-type {14} ' - command_line += '--psycris-inifile {16}' - if keep_profiles: - command_line += ' --keep_profiles' - if keep_photos: - command_line += ' --keep_photos' - command_line = command_line.format(number_of_mpi_processes, - path_to_grid, - binary_exe, - star1_exe, - star2_exe, - inlist_binary_project, - inlist_star1_binary, - inlist_star2_binary, - inlist_star1_formation, - inlist_star2_formation, - star_history_columns, - binary_history_columns, - profile_columns, - run_directory, - grid_type, - path_to_run_grid_exec, - psycris_inifile) - return command_line ############################################################################### # BEGIN MAIN FUNCTION @@ -985,11 +963,12 @@ if __name__ == '__main__': ########################################################################### args = parse_commandline() + verbose = args.verbose if args.verbose else False try: os.environ['MESA_DIR'] except: raise ValueError("MESA_DIR must be defined in your environment " - "before you can run a grid os MESA runs") + "before you can run a grid of MESA runs") # Determine location of executables proc = subprocess.Popen(['which', 'posydon-run-grid'], @@ -1003,11 +982,11 @@ if __name__ == '__main__': else: path_to_run_grid_exec = path_to_run_grid_exec.decode('utf-8').strip('\n') - run_parameters, slurm, mesa_inlists, mesa_extras = configfile.parse_inifile(args.inifile) - - if 'scenario' not in mesa_inlists.keys(): - mesa_inlists['scenario'] = None + run_parameters, slurm, user_mesa_inlists, user_mesa_extras = configfile.parse_inifile(args.inifile) + # Add default values for run parameters if not provided + # + # move to separate function in this file to setup these defaults if 'keep_profiles' not in run_parameters.keys(): run_parameters['keep_profiles'] = False @@ -1020,232 +999,157 @@ if __name__ == '__main__': if ('psycris_inifile' not in run_parameters.keys()) and (args.grid_type == 'dynamic'): raise ValueError("Please add psycris inifile to the [run_parameters] section of the inifile.") - if mesa_inlists['scenario'] is not None: - find_inlist_from_scenario(source=mesa_inlists['scenario'][0], - gitcommit=mesa_inlists['scenario'][1], - system_type=mesa_inlists['scenario'][2]) - # read grid - if '.csv' in run_parameters['grid']: - grid_df = pandas.read_csv(run_parameters['grid']) - fixgrid_file_name = run_parameters['grid'] - elif '.h5' in run_parameters['grid']: - psy_grid = PSyGrid() - psy_grid.load(run_parameters['grid']) - grid_df = psy_grid.get_pandas_initial_final() - psy_grid.close() - fixgrid_file_name = run_parameters['grid'] - elif os.path.isdir(run_parameters['grid']): - mygrid = PSyGrid().create(run_parameters['grid'], "./fixed_grid_results.h5", slim=True) - psy_grid = PSyGrid() - psy_grid.load("./fixed_grid_results.h5") - grid_df = psy_grid.get_pandas_initial_final() - psy_grid.close() - fixgrid_file_name = os.path.join(os.getcwd(), "fixed_grid_results.h5") - else: - raise ValueError('Grid format not recognized, please feed in an acceptable format: csv') - - # validate mesa_extras dictionary - # check if user has supplied multiple run_star run_binary extras files and enforce mesa, then posydon, then user order - extras_files_types = sorted(set([k.split('_')[0] for k in mesa_extras.keys() if ('binary_extras' in k)])) - print("WE ARE USING THE EXTRA FILE FROM TYPE {0}".format(extras_files_types[-1])) - for k in mesa_extras.keys(): - if ('binary_extras' in k) and (extras_files_types[-1] not in k): - Pwarn("Section mesa_extras value {0} is being set to".format(k)+\ - " None", "ReplaceValueWarning") - mesa_extras[k] = None - - binary_exe, star1_exe, star2_exe = make_executables(mesa_extras=mesa_extras, - working_directory=args.run_directory) - - if args.grid_type == "dynamic": - dynamic_grid_params = parse_inifile(run_parameters["psycris_inifile"]) - mesa_params_to_run_grid_over = dynamic_grid_params["posydon_dynamic_sampling_kwargs"]["mesa_column_names"] - inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, \ - inlist_star2_binary = construct_static_inlist(mesa_inlists, - grid_parameters=mesa_params_to_run_grid_over, - working_directory=args.run_directory) + # Check if a base is provided for the run + if 'base' in user_mesa_inlists.keys() and user_mesa_inlists['base'] is not None and user_mesa_inlists['base'] != "": + print('base provided in inifile, copying base to run directory') + # check if inlist_repository is provided + if 'inlist_repository' not in user_mesa_inlists.keys(): + print("No inlist_repository provided in inifile") + print('Using your home folder as the inlist repository') + user_mesa_inlists['inlist_repository'] = os.path.expanduser('~') else: - inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, \ - inlist_star2_binary = construct_static_inlist(mesa_inlists, - grid_parameters=grid_df.columns, - working_directory=args.run_directory) - - # handle column lists - # first, creating a directory - column_lists_folder = os.path.join(args.run_directory, 'column_lists') - if os.path.exists(column_lists_folder): shutil.rmtree(column_lists_folder) - os.makedirs(column_lists_folder) - # second, getting new location - star_history_columns = os.path.join(column_lists_folder, 'history_columns.list') - binary_history_columns = os.path.join(column_lists_folder, 'binary_history_columns.list') - profile_columns = os.path.join(column_lists_folder, 'profile_columns.list') - # third, copy lists - shutil.copy(mesa_inlists['star_history_columns'], star_history_columns) - shutil.copy(mesa_inlists['binary_history_columns'], binary_history_columns) - shutil.copy(mesa_inlists['profile_columns'], profile_columns) - - # now we can write the mpi command line + raise ValueError("Please provide a base for the MESA run in the configuration file") + + # setup the inlist repository + # MESA_version_base_path is the path to the base of the version in the inlist repository + MESA_version_root_path = setup_inlist_repository(user_mesa_inlists['inlist_repository'], + user_mesa_inlists['mesa_version']) + + # Setup the MESA_default, which is always needed + MESA_default_inlists, \ + MESA_default_extras, \ + MESA_default_columns = setup_MESA_defaults(MESA_version_root_path) + + # Setup POSYDON configuration (handles MESA base internally) + POSYDON_inlists, \ + POSYDON_extras, \ + POSYDON_columns = setup_POSYDON(MESA_version_root_path, + user_mesa_inlists['base'], + user_mesa_inlists['system_type']) + + # extract user inlists, extras and columns + user_inlists, \ + user_extras, \ + user_columns = setup_user(user_mesa_inlists, + user_mesa_extras) + + # build the final columns dictionary + final_columns = resolve_columns(MESA_default_columns, + POSYDON_columns, + user_columns) + final_extras = resolve_extras(MESA_default_extras, + POSYDON_extras, + user_extras) + + # Read grid to get grid parameters + nr_systems, grid_parameters, fixgrid_file_name = read_grid_file(run_parameters['grid']) + + # Extract output settings from user configuration + user_output_settings = get_additional_user_settings(mesa_inlists=user_mesa_inlists) + + # Stack all inlist layers together: MESA → POSYDON → user → grid → output + # This returns a dictionary of inlist parameters and their final values + final_inlists = resolve_inlists(MESA_default_inlists, + POSYDON_inlists, + user_inlists, + grid_parameters=grid_parameters, + output_settings=user_output_settings, + system_type=user_mesa_inlists['system_type'], + verbose=verbose) + + # Setup the run directory with all necessary files + # This also creates the inlists for the grid run + output_paths = setup_grid_run_folder(args.run_directory, + final_columns, + final_extras, + final_inlists, + verbose=verbose) + + # Now we can create the mpi command line to run the grid if slurm['job_array']: command_line = construct_command_line(1, - run_parameters['grid'], - binary_exe, - star1_exe, - star2_exe, - inlist_binary_project, - inlist_star1_binary, - inlist_star2_binary, - inlist_star1_formation, - inlist_star2_formation, - star_history_columns, - binary_history_columns, - profile_columns, - args.run_directory, - 'fixed', - path_to_run_grid_exec, - keep_profiles=run_parameters['keep_profiles'], - keep_photos=run_parameters['keep_photos']) + fixgrid_file_name, + output_paths['binary_executable'], + output_paths['star1_executable'], + output_paths['star2_executable'], + output_paths['inlist_binary_project'], + output_paths['inlist_star1_binary'], + output_paths['inlist_star2_binary'], + None, # inlist_star1_formation + None, # inlist_star2_formation, + output_paths['star_history_columns'], + output_paths['binary_history_columns'], + output_paths['profile_columns'], + args.run_directory, + 'fixed', + path_to_run_grid_exec, + keep_profiles=run_parameters['keep_profiles'], + keep_photos=run_parameters['keep_photos'] + ) command_line += ' --grid-point-index $SLURM_ARRAY_TASK_ID' - else: - command_line = construct_command_line(slurm['number_of_mpi_tasks']*slurm['number_of_nodes'], - fixgrid_file_name, - binary_exe, - star1_exe, - star2_exe, - inlist_binary_project, - inlist_star1_binary, - inlist_star2_binary, - inlist_star1_formation, - inlist_star2_formation, - star_history_columns, - binary_history_columns, - profile_columns, - args.run_directory, - args.grid_type, - path_to_run_grid_exec, - psycris_inifile = run_parameters["psycris_inifile"], - keep_profiles=run_parameters['keep_profiles'], - keep_photos=run_parameters['keep_photos']) + if args.submission_type == 'slurm': command_line += ' --job_end $SLURM_JOB_END_TIME' if 'work_dir' in slurm.keys() and not(slurm['work_dir'] == ''): command_line += ' --temporary-directory '+slurm['work_dir'] - # now we need to know how this person plans to run the above created - # command. As a shell script? As a SLURM submission? In some other way? - if args.submission_type == 'shell': - if os.path.exists('grid_command.sh'): - Pwarn('Replace grid_command.sh', "OverwriteWarning") - with open('grid_command.sh', 'w') as f: - f.write('#!/bin/bash\n\n') - f.write('export OMP_NUM_THREADS={0}\n\n'.format(slurm['number_of_cpus_per_task'])) - f.write('export MESASDK_ROOT={0}\n'.format(os.environ['MESASDK_ROOT'])) - f.write('source $MESASDK_ROOT/bin/mesasdk_init.sh\n') - f.write('export MESA_DIR={0}\n\n'.format(os.environ['MESA_DIR'])) - if slurm['job_array']: - f.write('for SLURM_ARRAY_TASK_ID in ') - for i in range(len(grid_df)): - f.write('{0} '.format(i)) - f.write('; do ') - f.write(command_line) - if slurm['job_array']: - f.write(' ; done\n\n') - # do cleanup - f.write('compress-mesa .\n') - if 'newgroup' in slurm.keys(): - f.write('echo \"Change group to {0}\"\n'.format(slurm['newgroup'])) - f.write('chgrp -fR {0} .\n'.format(slurm['newgroup'])) - f.write('echo \"Change group permission to rwX at least\"\n') - f.write('chmod -fR g+rwX .\n') - f.write('\necho \"Done.\"') - # make the script executable - os.system("chmod 755 grid_command.sh") - - elif args.submission_type == 'slurm': - # if slurm will we submit as job array or MPI - if slurm['job_array']: - grid_script = 'job_array_grid_submit.slurm' - if os.path.exists(grid_script): - Pwarn('Replace '+grid_script, "OverwriteWarning") - with open(grid_script, 'w') as f: - f.write('#!/bin/bash\n') - - f.write('#SBATCH --account={0}\n'.format(slurm['account'])) - f.write('#SBATCH --partition={0}\n'.format(slurm['partition'])) - f.write('#SBATCH -N 1\n') - f.write('#SBATCH --array=0-{0}\n'.format(len(grid_df)-1)) - f.write('#SBATCH --cpus-per-task {0}\n'.format(slurm['number_of_cpus_per_task'])) - f.write('#SBATCH --ntasks-per-node 1\n') - f.write('#SBATCH --time={0}\n'.format(slurm['walltime'])) - f.write('#SBATCH --job-name=\"mesa_grid_\${SLURM_ARRAY_TASK_ID}\"\n') - f.write('#SBATCH --output=mesa_grid.%A_%a.out\n') - f.write('#SBATCH --mail-type=ALL\n') - f.write('#SBATCH --mail-user={0}\n'.format(slurm['email'])) - f.write('#SBATCH --mem-per-cpu=4G\n\n') - - f.write('export OMP_NUM_THREADS={0}\n\n'.format(slurm['number_of_cpus_per_task'])) - - f.write('export MESASDK_ROOT={0}\n'.format(os.environ['MESASDK_ROOT'])) - f.write('source $MESASDK_ROOT/bin/mesasdk_init.sh\n') - f.write('export MESA_DIR={0}\n\n\n'.format(os.environ['MESA_DIR'])) - f.write(command_line) - else: - grid_script = 'mpi_grid_submit.slurm' - if os.path.exists(grid_script): - Pwarn('Replace '+grid_script, "OverwriteWarning") - with open(grid_script, 'w') as f: - f.write('#!/bin/bash\n') - - f.write('#SBATCH --account={0}\n'.format(slurm['account'])) - f.write('#SBATCH --partition={0}\n'.format(slurm['partition'])) - f.write('#SBATCH -N {0}\n'.format(slurm['number_of_nodes'])) - f.write('#SBATCH --cpus-per-task {0}\n'.format(slurm['number_of_cpus_per_task'])) - f.write('#SBATCH --ntasks-per-node {0}\n'.format(slurm['number_of_mpi_tasks'])) - f.write('#SBATCH --time={0}\n'.format(slurm['walltime'])) - f.write('#SBATCH --output=\"mesa_grid.out\"\n') - f.write('#SBATCH --mail-type=ALL\n') - f.write('#SBATCH --mail-user={0}\n'.format(slurm['email'])) - f.write('#SBATCH --mem-per-cpu=4G\n\n') - - f.write('export OMP_NUM_THREADS={0}\n\n'.format(slurm['number_of_cpus_per_task'])) - - f.write('export MESASDK_ROOT={0}\n'.format(os.environ['MESASDK_ROOT'])) - f.write('source $MESASDK_ROOT/bin/mesasdk_init.sh\n') - f.write('export MESA_DIR={0}\n\n\n'.format(os.environ['MESA_DIR'])) - f.write(command_line) - # create a cleanup script - if os.path.exists('cleanup.slurm'): - Pwarn('Replace cleanup.slurm', "OverwriteWarning") - with open('cleanup.slurm', 'w') as f: - f.write('#!/bin/bash\n') - - f.write('#SBATCH --account={0}\n'.format(slurm['account'])) - f.write('#SBATCH --partition={0}\n'.format(slurm['partition'])) - f.write('#SBATCH -N 1\n') - f.write('#SBATCH --cpus-per-task 1\n') - f.write('#SBATCH --ntasks-per-node 1\n') - f.write('#SBATCH --time={0}\n'.format(slurm['walltime'])) - f.write('#SBATCH --job-name=\"mesa_grid_cleanup\"\n') - f.write('#SBATCH --output=mesa_cleanup.out\n') - f.write('#SBATCH --mail-type=ALL\n') - f.write('#SBATCH --mail-user={0}\n'.format(slurm['email'])) - f.write('#SBATCH --mem-per-cpu=4G\n\n') - - f.write('compress-mesa .\n') - if 'newgroup' in slurm.keys(): - f.write('echo \"Change group to {0}\"\n'.format(slurm['newgroup'])) - f.write('chgrp -fR {0} .\n'.format(slurm['newgroup'])) - f.write('echo \"Change group permission to rwX at least\"\n') - f.write('chmod -fR g+rwX .\n') - f.write('\necho \"Done.\"') - # create a runfile script - if os.path.exists('run_grid.sh'): - Pwarn('Replace run_grid.sh', "OverwriteWarning") - with open('run_grid.sh', 'w') as f: - f.write('#!/bin/bash\n') - f.write('ID_GRID=$(sbatch --parsable {0})\n'.format(grid_script)) - f.write('echo \"{0}'.format(grid_script)+' submitted as \"${ID_GRID}\n') - f.write('ID_cleanup=$(sbatch --parsable --dependency=afterany:${ID_GRID} ' - '--kill-on-invalid-dep=yes cleanup.slurm)\n') - f.write('echo \"cleanup.slurm submitted as \"${ID_cleanup}\n') - # make the runfile script executable - os.system("chmod 755 run_grid.sh") + + # if args.grid_type == "dynamic": + # dynamic_grid_params = parse_inifile(run_parameters["psycris_inifile"]) + # mesa_params_to_run_grid_over = dynamic_grid_params["posydon_dynamic_sampling_kwargs"]["mesa_column_names"] + # inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, \ + # inlist_star2_binary = construct_static_inlist(mesa_inlists, + # grid_parameters=mesa_params_to_run_grid_over, + # working_directory=args.run_directory) + + # now we can write the mpi command line + # if slurm['job_array']: + # command_line = construct_command_line(1, + # run_parameters['grid'], + # binary_exe, + # star1_exe, + # star2_exe, + # inlist_binary_project, + # inlist_star1_binary, + # inlist_star2_binary, + # inlist_star1_formation, + # inlist_star2_formation, + # star_history_columns, + # binary_history_columns, + # profile_columns, + # args.run_directory, + # 'fixed', + # path_to_run_grid_exec, + # keep_profiles=run_parameters['keep_profiles'], + # keep_photos=run_parameters['keep_photos']) + # command_line += ' --grid-point-index $SLURM_ARRAY_TASK_ID' + # else: + # command_line = construct_command_line(slurm['number_of_mpi_tasks']*slurm['number_of_nodes'], + # fixgrid_file_name, + # binary_exe, + # star1_exe, + # star2_exe, + # inlist_binary_project, + # inlist_star1_binary, + # inlist_star2_binary, + # inlist_star1_formation, + # inlist_star2_formation, + # star_history_columns, + # binary_history_columns, + # profile_columns, + # args.run_directory, + # args.grid_type, + # path_to_run_grid_exec, + # psycris_inifile = run_parameters["psycris_inifile"], + # keep_profiles=run_parameters['keep_profiles'], + # # keep_photos=run_parameters['keep_photos']) + + if 'work_dir' in slurm.keys() and slurm['work_dir']: + command_line += f' --temporary-directory {slurm["work_dir"]}' + + # Generate submission scripts + generate_submission_scripts(args.submission_type, command_line, slurm, nr_systems) + print("Setup complete! You can now submit your grid to the cluster.") + print("To submit, run the following command:") + if args.submission_type == 'slurm': + print(f" sbatch submit_slurm.sh") diff --git a/posydon/CLI/grids/__init__.py b/posydon/CLI/grids/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py new file mode 100644 index 0000000000..2aee43f408 --- /dev/null +++ b/posydon/CLI/grids/setup.py @@ -0,0 +1,1766 @@ +import argparse +import glob +import os +import shutil +import subprocess + +import pandas as pd + +from posydon.active_learning.psy_cris.utils import parse_inifile +from posydon.grids.psygrid import PSyGrid +from posydon.utils import configfile +from posydon.utils import gridutils as utils +from posydon.utils.posydonwarning import Pwarn + +# Define column types and their filenames +column_types = {'star_history_columns' :'history_columns.list', + 'binary_history_columns':'binary_history_columns.list', + 'profile_columns' :'profile_columns.list'} +column_filenames = ['history_columns.list', 'binary_history_columns.list', 'profile_columns.list'] + +# Define extras keys +extras_keys = ['makefile_binary', 'makefile_star', 'binary_run', + 'star_run', 'binary_extras', 'star_binary_extras', 'star1_extras', 'star2_extras',] + +# define inlist keys +inlist_keys = ['binary_controls', 'binary_job', + 'star1_controls', 'star1_job', + 'star2_controls', 'star2_job'] + +# ANSI color codes +GREEN = '\033[92m' +GRAY = '\033[90m' +CYAN = '\033[96m' +YELLOW = '\033[93m' +MAGENTA = '\033[95m' +RESET = '\033[0m' +BOLD = '\033[1m' + +def check_file_exist(file_path, raise_error=True): + """Check if a file exists at the given path + + Parameters + ---------- + file_path : str + Path to the file to check + raise_error : bool, optional + If True, raise ValueError when file doesn't exist. + If False, return boolean. Default is True. + + Returns + ------- + bool + True if file exists, False otherwise (only when raise_error=False) + """ + exists = os.path.exists(file_path) + + if not exists: + if raise_error: + print(f"File {file_path} does not exist") + raise ValueError(f"File {file_path} does not exist") + else: + return False + + return True + +def setup_inlist_repository(inlist_repository, MESA_version): + """Setup the inlist repository by creating it if it does not exist + + Parameters + ---------- + inlist_repository : str + Path to the inlist repository where we will store inlists + base : str + Path to the base to use for the run + """ + POSYDON_inlist_URL = 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git' + print("We are setting up your inlist repository now") + + # check if the inlist repository path exists + if not os.path.exists(inlist_repository): + print(f"Creating inlist repository at {inlist_repository}") + os.makedirs(inlist_repository) + + # check if it contains anything and if not, clone the repo + if os.listdir(inlist_repository): + print(os.listdir(inlist_repository)) + print("Files found in inlist repository, assuming POSYDON inlist repository is already cloned!") + else: + out = subprocess.run(['git', 'clone', POSYDON_inlist_URL, inlist_repository], + capture_output=True, + text=True, + check=True,) + print(out.stdout) + + # update the repository + print("Updating inlist repository") + # TODO: Re-enable git pull after testing + print("Currently disabled for testing purposes") + # out = subprocess.run(['git', 'pull'], + # cwd=inlist_repository, + # capture_output=True, + # text=True, + # check=True,) + # print(out.stdout) + + # check if the base is available as a folder in the repository + version_root_path = os.path.join(inlist_repository, MESA_version) + if not os.path.exists(version_root_path): + print(version_root_path) + raise ValueError("The provided MESA version does not exist in the inlist repository, please check your provided MESA version and try again.") + + return version_root_path + +def setup_MESA_defaults(path_to_version): + """Setup the MESA default base inlists, extras and columns + + Parameters + ---------- + path_to_version : str + Path to the MESA version in the inlist repository + (root directory of the version) + + Returns + ------- + MESA_default_inlists : dict + Dictionary of MESA default inlists paths + MESA_default_extras : dict + Dictionary of MESA default extras paths + MESA_default_columns : dict + Dictionary of MESA default column files paths + """ + MESA_DIR = os.environ['MESA_DIR'] + + #---------------------------------- + # Inlists + #---------------------------------- + # Common inlists + # TODO: These are currently stored in the POSYDON inlist repository: + # The default MESA inlists with r11701 has a bug and we needed to fix it. + # We can add this to our changed MESA version, when we release it? + # Then this can be reverted to the MESA default inlists! + MESA_default_inlists = {} + + MESA_defaults_inlists_path = os.path.join(path_to_version, + 'MESA_defaults', + 'inlists') + MESA_default_inlists['binary_controls'] = [os.path.join(MESA_defaults_inlists_path, + 'binary', + 'binary_controls.defaults')] + MESA_default_inlists['binary_job'] = [os.path.join(MESA_defaults_inlists_path, + 'binary', + 'binary_job.defaults')] + MESA_default_inlists['star1_controls'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'controls.defaults')] + MESA_default_inlists['star1_job'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'star_job.defaults')] + MESA_default_inlists['star2_controls'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'controls.defaults')] + MESA_default_inlists['star2_job'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'star_job.defaults')] + + #---------------------------------- + # EXTRAS + #---------------------------------- + MESA_default_extras = {} + + # Helper to build MESA work directory paths + def mesa_path(module, *parts): + return os.path.join(MESA_DIR, module, 'work', *parts) + + # Makefiles + MESA_default_extras['makefile_binary'] = mesa_path('binary', + 'make', + 'makefile') + MESA_default_extras['makefile_star'] = mesa_path('star', + 'make', + 'makefile') + + # Run files + MESA_default_extras['star_run'] = mesa_path('star', + 'src', + 'run.f') + MESA_default_extras['binary_run'] = mesa_path('binary', + 'src', + 'binary_run.f') + + # Extras files for binary evolution + MESA_default_extras['binary_extras'] = mesa_path('binary', + 'src', + 'run_binary_extras.f') + MESA_default_extras['star_binary_extras'] = mesa_path('binary', + 'src', + 'run_star_extras.f') + + # star1_extras and star2_extras are needed for pre-MS formation steps (if any). + # During binary evolution, star_binary_extras is used for both stars. + # #Both stars use the same single-star module extras file + star_extras_path = mesa_path('star', 'src', 'run_star_extras.f') + MESA_default_extras['star1_extras'] = star_extras_path + MESA_default_extras['star2_extras'] = star_extras_path + + # Verify all extras files exist + for _, path in MESA_default_extras.items(): + check_file_exist(path) + + #---------------------------------- + # Columns + #---------------------------------- + + # Column files from MESA defaults + MESA_default_columns = { + 'star_history_columns': os.path.join(MESA_DIR, 'star', + 'defaults', 'history_columns.list'), + 'binary_history_columns': os.path.join(MESA_DIR, 'binary', + 'defaults', 'binary_history_columns.list'), + 'profile_columns': os.path.join(MESA_DIR, 'star', + 'defaults', 'profile_columns.list') + } + + # Verify all column files exist + for _, path in MESA_default_columns.items(): + check_file_exist(path) + + return MESA_default_inlists, MESA_default_extras, MESA_default_columns + +def setup_POSYDON(path_to_version, base, system_type): + """Setup the POSYDON inlists, extras and columns + + Parameters + ---------- + path_to_version : str + Path to the POSYDON version in the inlist repository + (root directory of the version) + base : str + Path to the POSYDON version to use for the run. + If "MESA", returns "empty" dictionaries. + system_type : str + Type of binary system + + Returns + ------- + POSYDON_inlists : dict + Dictionary of POSYDON inlists paths + POSYDON_extras : dict + Dictionary of POSYDON extras paths + POSYDON_columns : dict + Dictionary of POSYDON column files paths + """ + # If user wants to use MESA base only, return empty dictionaries + if base == "MESA": + POSYDON_columns = {name: None for name in column_types} + POSYDON_inlists = {} + POSYDON_extras = {key: None for key in extras_keys} + return POSYDON_inlists, POSYDON_extras, POSYDON_columns + + # Setup POSYDON base path + POSYDON_path = os.path.join(path_to_version, base) + check_file_exist(POSYDON_path) + + #---------------------------------- + # Inlists + #---------------------------------- + POSYDON_inlists = {} + # Common inlists + # TODOL these are not all needed for single stars. + common_inlists_path = os.path.join(POSYDON_path, 'common_inlists') + POSYDON_inlists['binary_controls'] = [os.path.join(common_inlists_path, 'inlist_project')] + POSYDON_inlists['binary_job'] = [os.path.join(common_inlists_path, 'inlist_project')] + # setup star1 inlists for binaries + POSYDON_inlists['star1_controls'] = [os.path.join(common_inlists_path, 'inlist1')] + POSYDON_inlists['star1_job'] = [os.path.join(common_inlists_path, 'inlist1')] + # setup star2 inlists for binaries + POSYDON_inlists['star2_controls'] = [os.path.join(common_inlists_path, 'inlist2')] + POSYDON_inlists['star2_job'] = [os.path.join(common_inlists_path, 'inlist2')] + + + if system_type == 'single_HMS': + # setup the paths to extra single star inlists + single_star_inlist_path = os.path.join(POSYDON_path, + 'single_HMS', + 'single_star_inlist') + POSYDON_inlists['star1_controls'].append(single_star_inlist_path) + elif system_type == 'single_HeMS': + # setup the paths to extra single star He inlists + # The HeMS single star inlists have two steps + # step 1: create HeMS star + # step 2: evolve HeMS star + helium_star_inlist_step1 = os.path.join(POSYDON_path, + 'single_HeMS', + 'inlist_step1') + helium_star_inlist_step2 = os.path.join(POSYDON_path, + 'single_HeMS', + 'inlist_step2') + # We need to also include the HMS single star inlist to set up the single star evolution + single_star_inlist_path = os.path.join(POSYDON_path, + 'single_HMS', + 'single_star_inlist') + + single_helium_inlists = [helium_star_inlist_step1, + helium_star_inlist_step2, + single_star_inlist_path] + + # the helium star setup steps contain control & job in the same inlist files + POSYDON_inlists['star1_controls'].extend(single_helium_inlists) + # We don't need to add the single star inlist again for the hob, + # since it only contains controls section in the file + POSYDON_inlists['star1_job'].extend(single_helium_inlists) + + elif system_type in ['HMS-HeMS', 'HeMS-HeMS']: + pass + elif system_type in ['CO-HMS', 'CO-HeMS']: + pass + elif system_type in ['HMS-HMS']: + # the common inlists are sufficient + pass + else: + raise ValueError(f"System type {system_type} not recognized.") + + #---------------------------------- + # Extras + #---------------------------------- + + POSYDON_extras = {} + POSYDON_extras['binary_extras'] = os.path.join(POSYDON_path, + 'extras_files', + 'run_binary_extras.f') + POSYDON_extras['star_binary_extras'] = os.path.join(POSYDON_path, + 'extras_files', + 'run_star_extras.f') + POSYDON_extras['star1_extras'] = os.path.join(POSYDON_path, + 'extras_files', + 'run_star_extras.f') + + #---------------------------------- + # Columns + #---------------------------------- + + # Setup POSYDON columns + POSYDON_columns = {} + for name, filename in column_types.items(): + file = os.path.join(POSYDON_path, 'column_files', filename) + # only add if the file exists + if check_file_exist(file, raise_error=False): + POSYDON_columns[name] = file + else: + POSYDON_columns[name] = None + + return POSYDON_inlists, POSYDON_extras, POSYDON_columns + +def setup_user(user_mesa_inlists, user_mesa_extras): + """Separates out user inlists, extras and columns + + Parameters + ---------- + user_mesa_inlists : dict + Dictionary of user inlists paths from the inifile + user_mesa_extras : dict + Dictionary of user extras paths from the inifile + + + Returns + ------- + user_mesa_inlists : dict + Dictionary of user inlists paths + user_mesa_extras : dict + Dictionary of user extras paths + user_columns : dict + Dictionary of user column files paths + """ + + #---------------------------------- + # Inlists + #---------------------------------- + # separate out inlists from user inlists + user_inlists = {} + for key in inlist_keys: + if key not in user_mesa_inlists.keys(): + user_inlists[key] = [] + else: + user_inlists[key] = [user_mesa_inlists[key]] + check_file_exist(user_mesa_inlists[key]) + + #---------------------------------- + # Extras + #---------------------------------- + # separate out extras from user inlists + + user_extras = {} + for key in extras_keys: + if key in user_mesa_extras.keys(): + user_extras[key] = user_mesa_extras[key] + check_file_exist(user_extras[key]) + else: + user_extras[key] = None + + #---------------------------------- + # Columns + #---------------------------------- + # separate out columns from user inlists + + user_columns = {} + + # separate out columns from user inlists + for name in column_types: + if name in user_mesa_inlists.keys(): + user_columns[name] = user_mesa_inlists[name] + check_file_exist(user_columns[name]) + else: + user_columns[name] = None + + return user_inlists, user_extras, user_columns + + +def resolve_configuration(keys, MESA_defaults, POSYDON_config, user_config, verbose=False): + """Resolve final configuration to use based on priority: + user_config > POSYDON_config > MESA_defaults + + Parameters + ---------- + keys : list + List of keys to iterate over for resolution + MESA_defaults : dict + Dictionary of MESA default paths + POSYDON_config : dict + Dictionary of POSYDON configuration paths + user_config : dict + Dictionary of user configuration paths + verbose : bool, optional + If True, print a visual priority table. Default is False. + + Returns + ------- + final_config : dict + Dictionary of final configuration paths to use + """ + if verbose: + print_priority_table(keys, MESA_defaults, POSYDON_config, user_config) + + final_config = {} + + for key in keys: + if user_config.get(key) is not None: + final_config[key] = user_config[key] + elif POSYDON_config.get(key) is not None: + final_config[key] = POSYDON_config[key] + else: + final_config[key] = MESA_defaults.get(key) + + return final_config + + +def resolve_columns(MESA_default_columns, POSYDON_columns, user_columns, verbose=False): + """Resolve final columns to use based on priority: + user_columns > POSYDON_columns > MESA_default_columns + + Parameters + ---------- + MESA_default_columns : dict + Dictionary of MESA default column files paths + POSYDON_columns : dict + Dictionary of POSYDON column files paths + user_columns : dict + Dictionary of user column files paths + verbose : bool, optional + If True, print a visual priority table. Default is False. + + Returns + ------- + final_columns : dict + Dictionary of final column files paths to use + """ + if verbose: + print_priority_table(column_types, MESA_default_columns, + POSYDON_columns, user_columns, + title="Column Files Priority") + return resolve_configuration(column_types, MESA_default_columns, + POSYDON_columns, user_columns, verbose=False) + + +def resolve_extras(MESA_default_extras, POSYDON_extras, user_extras, verbose=False): + """Resolve final extras to use based on priority: + user_extras > POSYDON_extras > MESA_default_extras + + Parameters + ---------- + MESA_default_extras : dict + Dictionary of MESA default extras paths + POSYDON_extras : dict + Dictionary of POSYDON extras paths + user_extras : dict + Dictionary of user extras paths + verbose : bool, optional + If True, print a visual priority table. Default is False. + + Returns + ------- + final_extras : dict + Dictionary of final extras paths to use + """ + if verbose: + print_priority_table(extras_keys, MESA_default_extras, + POSYDON_extras, user_extras, + title="EXTRAS Files Priority") + return resolve_configuration(extras_keys, MESA_default_extras, + POSYDON_extras, user_extras, verbose=False) + + +def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, title="Configuration Priority"): + """Print a visual table showing which configuration layer is used for each key. + + Parameters + ---------- + keys : list + List of keys to display + MESA_defaults : dict + Dictionary of MESA default paths + POSYDON_config : dict + Dictionary of POSYDON configuration paths + user_config : dict + Dictionary of user configuration paths + title : str, optional + Title for the table + """ + + # Find the longest key name for formatting + max_key_len = max(len(str(key)) for key in keys) + col_width = 12 + + # Print title + print(f"\n{BOLD}{title}{RESET}") + print("=" * (max_key_len + col_width * 3 + 4)) + + # Print header with colored column names + header = f"{'Key':<{max_key_len}} {CYAN}{'MESA':^{col_width}}{RESET}{YELLOW}{'POSYDON':^{col_width}}{RESET}{MAGENTA}{'user':^{col_width}}{RESET}" + print(f"{BOLD}{header}{RESET}") + print("-" * (max_key_len + col_width * 3 + 4)) + + # Print each row + for key in keys: + # Check which configs have this key + has_mesa = MESA_defaults.get(key) is not None + has_posydon = POSYDON_config.get(key) is not None + has_user = user_config.get(key) is not None + + # Determine which one is used (priority: user > POSYDON > MESA) + used = 'user' if has_user else ('POSYDON' if has_posydon else ('MESA' if has_mesa else None)) + + # Format each column + mesa_mark = 'x' if has_mesa else ' ' + posydon_mark = 'x' if has_posydon else ' ' + user_mark = 'x' if has_user else ' ' + + # Apply colors + if used == 'MESA': + mesa_str = f"{GREEN}{mesa_mark}{RESET}" + posydon_str = f"{GRAY}{posydon_mark}{RESET}" + user_str = f"{GRAY}{user_mark}{RESET}" + elif used == 'POSYDON': + mesa_str = f"{GRAY}{mesa_mark}{RESET}" + posydon_str = f"{GREEN}{posydon_mark}{RESET}" + user_str = f"{GRAY}{user_mark}{RESET}" + elif used == 'user': + mesa_str = f"{GRAY}{mesa_mark}{RESET}" + posydon_str = f"{GRAY}{posydon_mark}{RESET}" + user_str = f"{GREEN}{user_mark}{RESET}" + else: + mesa_str = f"{GRAY}{mesa_mark}{RESET}" + posydon_str = f"{GRAY}{posydon_mark}{RESET}" + user_str = f"{GRAY}{user_mark}{RESET}" + + # Print row + print(f"{key:<{max_key_len}} {mesa_str:^{col_width+9}}{posydon_str:^{col_width+9}}{user_str:^{col_width+9}}") + + print("=" * (max_key_len + col_width * 3 + 4)) + print(f"{GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used\n") + +def print_inlist_stacking_table(keys, MESA_defaults, POSYDON_config, user_config, title="Inlist Stacking"): + """Print a visual table showing how inlists are stacked for each key. + + Unlike extras and columns which use replacement, inlists stack together where + each layer contributes files. The final inlist for each key contains entries + from user config (highest priority), then POSYDON config, then MESA defaults (lowest priority). + + Parameters + ---------- + keys : list + List of inlist keys to display + MESA_defaults : dict + Dictionary of MESA default inlist paths (each value is a list) + POSYDON_config : dict + Dictionary of POSYDON configuration inlist paths (each value is a list) + user_config : dict + Dictionary of user configuration inlist paths (each value is a list) + title : str, optional + Title for the table + """ + # Find the longest key name for formatting + max_key_len = max(len(str(key)) for key in keys) + + # Print title + print(f"\n{BOLD}{title}{RESET}") + print("=" * 80) + print(f"{BOLD}{'Key':<{max_key_len}} Layer Stack{RESET}") + print("-" * 80) + + # Print each row + for key in keys: + mesa_list = MESA_defaults.get(key, []) or [] + posydon_list = POSYDON_config.get(key, []) or [] + user_list = user_config.get(key, []) or [] + + # Count total files + total = len(mesa_list) + len(posydon_list) + len(user_list) + + if total == 0: + print(f"{key:<{max_key_len}} {GRAY}(no inlists){RESET}") + continue + + # Print key with count + print(f"{BOLD}{key:<{max_key_len}}{RESET} ({total} file{'s' if total != 1 else ''})") + + # Print each layer with indentation (highest priority first) + layer_num = 1 + + for inlist_path in user_list: + filename = os.path.basename(inlist_path) + print(f"{'':>{max_key_len}} {MAGENTA}[{layer_num}]{RESET} {GRAY}user:{RESET} {filename}") + layer_num += 1 + + for inlist_path in posydon_list: + filename = os.path.basename(inlist_path) + print(f"{'':>{max_key_len}} {YELLOW}[{layer_num}]{RESET} {GRAY}POSYDON:{RESET} {filename}") + layer_num += 1 + + for inlist_path in mesa_list: + filename = os.path.basename(inlist_path) + print(f"{'':>{max_key_len}} {CYAN}[{layer_num}]{RESET} {GRAY}MESA:{RESET} {filename}") + layer_num += 1 + + print() # Blank line between keys + + print("=" * 80) + print(f"Note: Files at the top override parameters from files below") + print(f"{MAGENTA}user{RESET} (highest priority) → {YELLOW}POSYDON{RESET} (config) → {CYAN}MESA{RESET} (base)\n") + +def print_inlist_parameter_override_table(key, mesa_params, posydon_params, user_params, final_params, show_details=False): + """Print a table showing which layer each parameter comes from, similar to extras/columns tables. + + Parameters + ---------- + key : str + The inlist key (e.g., 'binary_controls', 'star1_controls') + mesa_params : dict + Parameters from MESA defaults + posydon_params : dict + Parameters from POSYDON config + user_params : dict + Parameters from user config + final_params : dict + Final merged parameters + show_details : bool, optional + If True, show detailed parameter-by-parameter table. Default is False. + """ + # Get all unique parameter names + all_params = sorted(set(list(mesa_params.keys()) + + list(posydon_params.keys()) + + list(user_params.keys()))) + + if not all_params: + return + + # Only show detailed table if requested + if not show_details: + return + + # Find the longest parameter name for formatting + max_param_len = max(len(str(param)) for param in all_params) + max_param_len = max(max_param_len, 15) # Minimum width + col_width = 12 + + # Print summary header + overridden_count = sum(1 for p in all_params if (p in user_params and (p in mesa_params or p in posydon_params)) or + (p in posydon_params and p in mesa_params)) + print(f"\n {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") + print(" " + "=" * (max_param_len + col_width * 3 + 4)) + + # Print header with colored column names + header = f" {'Parameter':<{max_param_len}} {CYAN}{'MESA':^{col_width}}{RESET}{YELLOW}{'POSYDON':^{col_width}}{RESET}{MAGENTA}{'user':^{col_width}}{RESET}" + print(f"{BOLD}{header}{RESET}") + print(" " + "-" * (max_param_len + col_width * 3 + 4)) + + # Print each parameter row + for param in all_params: + # Check which configs have this parameter + has_mesa = param in mesa_params + has_posydon = param in posydon_params + has_user = param in user_params + + # Determine which one is used (priority: user > POSYDON > MESA) + used = 'user' if has_user else ('POSYDON' if has_posydon else ('MESA' if has_mesa else None)) + + # Format each column + mesa_mark = 'x' if has_mesa else ' ' + posydon_mark = 'x' if has_posydon else ' ' + user_mark = 'x' if has_user else ' ' + + # Apply colors - green for used, gray for available but not used + if used == 'MESA': + mesa_str = f"{GREEN}{mesa_mark}{RESET}" + posydon_str = f"{GRAY}{posydon_mark}{RESET}" + user_str = f"{GRAY}{user_mark}{RESET}" + elif used == 'POSYDON': + mesa_str = f"{GRAY}{mesa_mark}{RESET}" + posydon_str = f"{GREEN}{posydon_mark}{RESET}" + user_str = f"{GRAY}{user_mark}{RESET}" + elif used == 'user': + mesa_str = f"{GRAY}{mesa_mark}{RESET}" + posydon_str = f"{GRAY}{posydon_mark}{RESET}" + user_str = f"{GREEN}{user_mark}{RESET}" + else: + mesa_str = f"{GRAY}{mesa_mark}{RESET}" + posydon_str = f"{GRAY}{posydon_mark}{RESET}" + user_str = f"{GRAY}{user_mark}{RESET}" + + # Print row + print(f" {param:<{max_param_len}} {mesa_str:^{col_width+9}}{posydon_str:^{col_width+9}}{user_str:^{col_width+9}}") + + print(" " + "=" * (max_param_len + col_width * 3 + 4)) + print(f" {GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used") + + +def print_inlist_parameter_override_table_v2(key, layer_params, final_params, show_details=False): + """Print a table showing which layer each parameter comes from (supports all layers). + + Parameters + ---------- + key : str + The inlist key (e.g., 'binary_controls', 'star1_controls') + layer_params : dict + Dictionary mapping layer names to parameter dictionaries for this key + Format: {'MESA': {params}, 'POSYDON': {params}, 'user': {params}, 'grid': {params}, 'output': {params}} + final_params : dict + Final merged parameters + show_details : bool, optional + If True, show detailed parameter-by-parameter table. Default is False. + """ + if not show_details: + return + + # Get parameters for each layer for this specific key + mesa_params = layer_params.get('MESA', {}).get(key, {}) + posydon_params = layer_params.get('POSYDON', {}).get(key, {}) + user_params = layer_params.get('user', {}).get(key, {}) + grid_params = layer_params.get('grid', {}).get(key, {}) + output_params = layer_params.get('output', {}).get(key, {}) + + # Get all unique parameter names + all_params = sorted(set( + list(mesa_params.keys()) + + list(posydon_params.keys()) + + list(user_params.keys()) + + list(grid_params.keys()) + + list(output_params.keys()) + )) + + if not all_params: + return + + # Find the longest parameter name for formatting + max_param_len = max(len(str(param)) for param in all_params) + max_param_len = max(max_param_len, 15) # Minimum width + col_width = 10 + + # Color mapping for each layer + layer_colors = { + 'MESA': CYAN, + 'POSYDON': YELLOW, + 'user': MAGENTA, + 'grid': '\033[94m', # Blue + 'output': '\033[92m' # Green + } + + # Determine which layers have parameters for this key + active_layers = [] + for layer_name in ['MESA', 'POSYDON', 'user', 'grid', 'output']: + layer_data = layer_params.get(layer_name, {}).get(key, {}) + if layer_data: + active_layers.append(layer_name) + + # Count overridden parameters + overridden_count = 0 + for param in all_params: + count = sum(1 for layer_name in active_layers + if param in layer_params.get(layer_name, {}).get(key, {})) + if count > 1: + overridden_count += 1 + + # Print summary header + print(f"\n {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") + total_width = max_param_len + col_width * len(active_layers) + len(active_layers) * 2 + print(" " + "=" * total_width) + + # Print header with colored column names + header_parts = [f"{'Parameter':<{max_param_len}}"] + for layer_name in active_layers: + color = layer_colors.get(layer_name, RESET) + header_parts.append(f"{color}{layer_name:^{col_width}}{RESET}") + header = " ".join(header_parts) + print(f" {BOLD}{header}{RESET}") + print(" " + "-" * total_width) + + # Print each parameter row + for param in all_params: + # Determine which layer provides the final value (check in priority order: highest to lowest) + # Priority: output > grid > user > POSYDON > MESA + used_layer = None + for layer_name in ['output', 'grid', 'user', 'POSYDON', 'MESA']: + if param in layer_params.get(layer_name, {}).get(key, {}): + used_layer = layer_name + break # Found the highest priority layer with this parameter + + row_parts = [f"{param:<{max_param_len}}"] + for layer_name in active_layers: + has_param = param in layer_params.get(layer_name, {}).get(key, {}) + mark = 'x' if has_param else ' ' + color = layer_colors.get(layer_name, RESET) + + # Green if this layer provides the final value, gray if available but not used + if has_param and layer_name == used_layer: + row_parts.append(f"{GREEN}{mark:^{col_width}}{RESET}") + elif has_param: + row_parts.append(f"{GRAY}{mark:^{col_width}}{RESET}") + else: + row_parts.append(f"{mark:^{col_width}}") + + print(" " + " ".join(row_parts)) + + print(" " + "=" * total_width) + print(f" {GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used") + + + + +def print_inlist_summary_table_v2(all_keys, layer_counts): + """Print a summary table showing parameter counts per section at each layer. + + This version supports multiple layers including grid and output configurations. + + Parameters + ---------- + all_keys : list + List of inlist keys (sections) + layer_counts : dict + Dictionary mapping layer names to count dictionaries + Format: {'MESA': {key: count}, 'POSYDON': {key: count}, ...} + """ + # Color mapping for each layer + layer_colors = { + 'MESA': CYAN, + 'POSYDON': YELLOW, + 'user': MAGENTA, + 'grid': '\033[94m', # Blue + 'output': '\033[92m' # Green (reuse) + } + + # Find the longest key name for formatting + max_key_len = max(len(str(key)) for key in all_keys) + max_key_len = max(max_key_len, 15) # Minimum width + col_width = 10 + + # Get layers that have any parameters + active_layers = [layer for layer in ['MESA', 'POSYDON', 'user', 'grid', 'output'] + if layer in layer_counts and any(layer_counts[layer].values())] + + num_layers = len(active_layers) + total_width = max_key_len + col_width * num_layers + (num_layers + 1) + + print(f"\n{BOLD}Parameter Count Summary{RESET}") + print("=" * total_width) + + # Print header with colored column names + header_parts = [f"{'Section':<{max_key_len}}"] + for layer in active_layers: + color = layer_colors.get(layer, RESET) + header_parts.append(f"{color}{layer:^{col_width}}{RESET}") + header = " ".join(header_parts) + print(f"{BOLD}{header}{RESET}") + print("-" * total_width) + + # Print each section row + for key in all_keys: + row_parts = [f"{key:<{max_key_len}}"] + for layer in active_layers: + count = layer_counts[layer].get(key, 0) + color = layer_colors.get(layer, RESET) + row_parts.append(f"{color}{count:^{col_width}}{RESET}") + print(" ".join(row_parts)) + + print("=" * total_width) + print(f"\nLayer priority (lowest → highest): {' → '.join(active_layers)}\n") + +def _get_section_from_key(key): + """Determine the MESA inlist section based on the key name. + + Parameters + ---------- + key : str + The inlist key name + + Returns + ------- + str or None + The section name (e.g., '&binary_controls') or None + """ + if 'binary_controls' in key: + return '&binary_controls' + elif 'binary_job' in key: + return '&binary_job' + elif ('star1_job' in key) or ('star2_job' in key): + return '&star_job' + elif ('star1_controls' in key) or ('star2_controls' in key): + return '&controls' + else: + return None + +def _process_inlist_layer(inlist_paths, section): + """Process a single layer of inlist files and return merged parameters. + Files are processed in order, with later files overriding parameters from + earlier files. + + Parameters + ---------- + inlist_paths : list or None + List of file paths for this layer + section : str or None + The MESA section to extract + + Returns + ------- + dict + Merged parameters from all files in this layer + """ + layer_params = {} + if inlist_paths: + for file_path in inlist_paths: + inlist_dict = utils.clean_inlist_file(file_path, section=section)[section] + layer_params.update(inlist_dict) + return layer_params + +def _clean_inlist_parameters(params_dict): + """Clean inlist parameters by removing unwanted keys and replacing placeholders. + + Parameters + ---------- + params_dict : dict + Dictionary of parameters to clean + key : str + The inlist key (used for logging) + + Returns + ------- + dict + Cleaned parameters dictionary + """ + # Remove read_extra and inlist references (any parameter containing these substrings) + # This catches: read_extra, inlist, read_extra_controls_inlist1, inlist_names, etc. + cleaned = {k: v for k, v in params_dict.items() + if not any(substring in k for substring in ['read_extra', 'inlist'])} + + # Replace num_x_ctrls with actual index (e.g., num_x_ctrls -> 1) + # This is a special default that the default value in the .defaults + # file in MESA does not work because it is a placeholder + keys_to_replace = {k: k.replace('num_x_ctrls', '1') + for k in cleaned.keys() + if 'num_x_ctrls' in k} + + if keys_to_replace: + for old_key, new_key in keys_to_replace.items(): + cleaned[new_key] = cleaned.pop(old_key) + + return cleaned + +def _build_grid_parameter_layer(grid_parameters, final_inlists): + """Build a layer of inlist parameters for grid-specific configurations. + + Parameters + ---------- + grid_parameters : list or set + Collection of grid parameter names + final_inlists : dict + Current state of final inlists (to check which sections are affected) + + Returns + ------- + dict + Dictionary mapping section names to parameter dictionaries for grid config + """ + grid_layer = {} + + # Configuration for each section: (read_extra_param, extra_name_param, inlist_filename) + section_config = { + 'star1_controls': ('read_extra_controls_inlist1', + 'extra_controls_inlist1_name', + 'inlist_grid_star1_binary_controls'), + 'star2_controls': ('read_extra_controls_inlist1', + 'extra_controls_inlist1_name', + 'inlist_grid_star2_binary_controls'), + 'star1_job': ('read_extra_star_job_inlist1', + 'extra_star_job_inlist1_name', + 'inlist_grid_star1_job'), + 'star2_job': ('read_extra_star_job_inlist1', + 'extra_star_job_inlist1_name', + 'inlist_grid_star2_job'), + } + + # Check which sections have grid parameters + for section, config in section_config.items(): + matching_params = [param for param in grid_parameters + if param in final_inlists.get(section, {})] + + if matching_params: + read_param, name_param, filename = config + grid_layer[section] = { + read_param: '.true.', + name_param: f"'{filename}'" + } + else: + grid_layer[section] = {} + + # + print('Adding grid parameters to sections:', + ', '.join([sec for sec, params in grid_layer.items() if params])) + + return grid_layer + + +def _build_output_controls_layer(output_settings): + """Build a layer of inlist parameters for output control configurations. + + Parameters + ---------- + output_settings : dict + Dictionary of output settings from the configuration file + + Returns + ------- + dict + Dictionary mapping section names to parameter dictionaries for output controls + """ + # Convert boolean to MESA Fortran string + def to_fortran_bool(value): + return ".true." if value else ".false." + + output_layer = { + 'binary_controls': {}, + 'binary_job': {}, + 'star1_controls': {}, + 'star1_job': {}, + 'star2_controls': {}, + 'star2_job': {} + } + + # Configuration: (config_key, section, enabled_param, filename_param, filename_value) + output_config = [ + ('final_profile_star1', 'star1_job', 'write_profile_when_terminate', + 'filename_for_profile_when_terminate', "'final_profile_star1.data'"), + ('final_profile_star2', 'star2_job', 'write_profile_when_terminate', + 'filename_for_profile_when_terminate', "'final_profile_star2.data'"), + ('final_model_star1', 'star1_job', 'save_model_when_terminate', + 'save_model_filename', "'final_star1.mod'"), + ('final_model_star2', 'star2_job', 'save_model_when_terminate', + 'save_model_filename', "'final_star2.mod'"), + ('history_star1', 'star1_controls', 'do_history_file', None, None), + ('history_star2', 'star2_controls', 'do_history_file', None, None), + ] + + # Process each output configuration + for config_key, section, enabled_param, filename_param, filename_value in output_config: + if config_key in output_settings: + is_enabled = output_settings[config_key] + output_layer[section][enabled_param] = to_fortran_bool(is_enabled) + + # Set filename parameter if provided and feature is enabled + if filename_param and is_enabled: + output_layer[section][filename_param] = filename_value + + # Handle history_interval (applies to all sections) + if 'history_interval' in output_settings: + interval = output_settings['history_interval'] + output_layer['binary_controls']['history_interval'] = interval + output_layer['star1_controls']['history_interval'] = interval + output_layer['star2_controls']['history_interval'] = interval + + # Disable binary history if requested + if 'binary_history' in output_settings and not output_settings['binary_history']: + output_layer['binary_controls']['history_interval'] = "-1" + + + print('Adding output control parameters to sections:', + ', '.join([sec for sec, params in output_layer.items() if params])) + + # Handle ZAMS filenames if provided + if 'zams_filename_1' in output_settings and output_settings['zams_filename_1'] is not None: + output_layer['star1_controls']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" + + if 'zams_filename_2' in output_settings and output_settings['zams_filename_2'] is not None: + output_layer['star2_controls']['zams_filename'] = f"'{output_settings['zams_filename_2']}'" + + return output_layer + + +def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_type, + grid_parameters=None, output_settings=None, verbose=False, show_details=False): + """Resolve final inlists to use based on priority: + output_settings > grid_parameters > user_inlists > POSYDON_inlists > MESA_default_inlists + + The inlists are stacked lists, so the final inlist for each key + contains all layers in priority order. + + Examples + ------- + MESA_default_inlist contains + - `alpha_semiconvection = 0.1d0` + POSYDON_inlist contains + - `alpha_semiconvection = 0.2d0` + user_inlist contains + - `alpha_semiconvection = 0.15d0` + The final inlist will contain + - `alpha_semiconvection = 0.15d0` + + Parameters + ---------- + MESA_default_inlists : dict + Dictionary of MESA default inlists paths + POSYDON_inlists : dict + Dictionary of POSYDON inlists paths + user_inlists : dict + Dictionary of user inlists paths + system_type : str + Type of binary system + grid_parameters : list or set, optional + Collection of grid parameter names. If provided, adds grid configuration layer. + output_settings : dict, optional + Dictionary of output settings. If provided, adds output control layer. + verbose : bool, optional + If True, print visual stacking and parameter count summary. Default is False. + show_details : bool, optional + If True, print detailed parameter-by-parameter tables. Default is False. + + Returns + ------- + final_inlists : dict + Dictionary where each key maps to parameter dictionaries + """ + # Get all unique keys from all dictionaries + all_keys = sorted(set(list(MESA_default_inlists.keys()) + + list(POSYDON_inlists.keys()) + + list(user_inlists.keys()))) + + # Stack inlists: MESA (base) → POSYDON → user → grid → output (highest priority) + final_inlists = {} + + # Track parameter counts for summary + layer_counts = { + 'MESA': {}, + 'POSYDON': {}, + 'user': {}, + 'grid': {}, + 'output': {} + } + + # Track layer parameters for detailed printing if requested + layer_params = { + 'MESA': {}, + 'POSYDON': {}, + 'user': {}, + 'grid': {}, + 'output': {} + } + + # First pass: process file-based layers (MESA, POSYDON, user) + for key in all_keys: + # Determine the section based on the key name + section = _get_section_from_key(key) + + # Process each file-based layer + mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), section) + posydon_layer_params = _process_inlist_layer(POSYDON_inlists.get(key), section) + user_layer_params = _process_inlist_layer(user_inlists.get(key), section) + + # Merge file-based layers (order matters: MESA first, then POSYDON, then user) + final_inlists[key] = {} + final_inlists[key].update(mesa_layer_params) + final_inlists[key].update(posydon_layer_params) + final_inlists[key].update(user_layer_params) + + # Store counts and parameters for summary + layer_counts['MESA'][key] = len(mesa_layer_params) + layer_counts['POSYDON'][key] = len(posydon_layer_params) + layer_counts['user'][key] = len(user_layer_params) + + layer_params['MESA'][key] = mesa_layer_params + layer_params['POSYDON'][key] = posydon_layer_params + layer_params['user'][key] = user_layer_params + + # Clean the final inlist parameters. + # Needs to happen before adding grid/output layers! + # because grid_parameters add read_extra parameters that need to be preserved! + for key in all_keys: + final_inlists[key] = _clean_inlist_parameters(final_inlists[key]) + + # Build grid configuration layer if provided + if grid_parameters: + grid_layer_dict = _build_grid_parameter_layer(grid_parameters, final_inlists) + for key in all_keys: + grid_params = grid_layer_dict.get(key, {}) + final_inlists[key].update(grid_params) + layer_counts['grid'][key] = len(grid_params) + layer_params['grid'][key] = grid_params + else: + for key in all_keys: + layer_counts['grid'][key] = 0 + layer_params['grid'][key] = {} + + # Build output controls layer if provided + if output_settings: + output_layer_dict = _build_output_controls_layer(output_settings) + for key in all_keys: + output_params = output_layer_dict.get(key, {}) + final_inlists[key].update(output_params) + layer_counts['output'][key] = len(output_params) + layer_params['output'][key] = output_params + else: + for key in all_keys: + layer_counts['output'][key] = 0 + layer_params['output'][key] = {} + + + if show_details: + for key in all_keys: + # Only show sections that have parameters in any layer + if any(layer_params[layer][key] for layer in layer_params): + print(f"\n{BOLD}═══ {key} ═══{RESET}") + print_inlist_parameter_override_table_v2( + key, + layer_params, + final_inlists[key], + show_details=True + ) + + + if verbose: + print_inlist_summary_table_v2(all_keys, layer_counts) + + # Return the final parameter dictionaries + return final_inlists + + +def read_grid_file(filepath): + """Read grid file and return grid parameters as a dictionary. + + Parameters + ---------- + filepath : str + Path to the grid file + + Returns + ------- + number of grid points : int + Number of grid points in the grid + grid parameter names : set + Set of grid parameter names (column names) + fixgrid_file_name : str + Path to the fixed grid file used for the grid + """ + # TODO: I"m not sure what the last option is for. + # Is it processing runs for a grid stored in a directory? + if '.csv' in filepath: + grid_df = pd.read_csv(filepath) + fixgrid_file_name = filepath + elif '.h5' in filepath: + psy_grid = PSyGrid() + psy_grid.load(filepath) + grid_df = psy_grid.get_pandas_initial_final() + psy_grid.close() + fixgrid_file_name = filepath + elif os.path.isdir(filepath): + PSyGrid().create(filepath, "./fixed_grid_results.h5", slim=True) + psy_grid = PSyGrid() + psy_grid.load("./fixed_grid_results.h5") + grid_df = psy_grid.get_pandas_initial_final() + psy_grid.close() + fixgrid_file_name = os.path.join(os.getcwd(), "fixed_grid_results.h5") + else: + raise ValueError('Grid format not recognized, please feed in an acceptable format: csv') + + grid_parameters = set(grid_df.columns.tolist()) + grid_parameters = [params.lower() for params in grid_parameters] + + return (len(grid_df), grid_parameters, fixgrid_file_name) + +def get_additional_user_settings(mesa_inlists): + """Extract additional settings from configuration for output controls. + + Parameters + ---------- + mesa_inlists : dict + Dictionary of user inlists and configuration + + Returns + ------- + dict + Dictionary of additional settings + """ + output_settings_list = ['history_interval', 'binary_history', + 'final_profile_star1', 'final_profile_star2', + 'final_model_star1', 'final_model_star2', + 'history_star1', 'history_star2', + 'zams_filename_1', 'zams_filename_2'] + output_settings = {} + + for setting in output_settings_list: + if setting in mesa_inlists: + output_settings[setting] = mesa_inlists[setting] + else: + output_settings[setting] = None + + # Handle single zams_filename case + if 'zams_filename' in mesa_inlists: + output_settings['zams_filename_1'] = mesa_inlists['zams_filename'] + output_settings['zams_filename_2'] = mesa_inlists['zams_filename'] + + + return output_settings + + + +def _write_inlist_section(f, section_name, parameters): + """Write a single MESA inlist section to file. + + Parameters + ---------- + f : file object + File handle opened in binary write mode + section_name : str + Name of the section (e.g., 'controls', 'binary_controls') + parameters : dict + Dictionary of parameter name -> value pairs + """ + f.write(f'&{section_name}\n\n'.encode('utf-8')) + for key, value in parameters.items(): + f.write(f'\t{key} = {value}\n'.encode('utf-8')) + f.write(f'\n/ ! end of {section_name} namelist\n'.encode('utf-8')) + + +def _write_binary_inlist(filepath, binary_controls, binary_job): + """Write the binary inlist_project file. + + Parameters + ---------- + filepath : str + Path to write the inlist file + binary_controls : dict + Parameters for binary_controls section + binary_job : dict + Parameters for binary_job section + """ + with open(filepath, 'wb') as f: + _write_inlist_section(f, 'binary_controls', binary_controls) + f.write(b'\n') + _write_inlist_section(f, 'binary_job', binary_job) + print(f'Wrote inlist: {filepath}') + + +def _write_star_inlist(filepath, star_controls, star_job): + """Write a star inlist file (for star1 or star2). + + Parameters + ---------- + filepath : str + Path to write the inlist file + star_controls : dict + Parameters for controls section + star_job : dict + Parameters for star_job section + """ + with open(filepath, 'wb') as f: + _write_inlist_section(f, 'controls', star_controls) + f.write(b'\n') + _write_inlist_section(f, 'star_job', star_job) + print(f'Wrote inlist: {filepath}') + + +def _create_build_script(path): + """Create the 'mk' build script for compiling MESA executables. + + Parameters + ---------- + path : str + Path to the grid run folder + """ + mk_filepath = os.path.join(path, 'mk') + if os.path.exists(mk_filepath): + print(f"Warning: 'mk' file already exists. It will be overwritten.") + + with open(mk_filepath, 'w') as f: + f.write(f'cd {os.path.join(path, "binary/make")}\n') + f.write(f'make -f makefile_binary\n') + f.write(f'cd {os.path.join(path, "star1/make")}\n') + f.write(f'make -f makefile_star\n') + f.write(f'cd {os.path.join(path, "star2/make")}\n') + f.write(f'make -f makefile_star\n') + + print(f'Created build script: {mk_filepath}') + subprocess.run(['chmod', '755', mk_filepath]) + subprocess.run(['./mk'], shell=True, cwd=path) + +def _copy_columns(path, final_columns): + """Copy column list files to the grid run folder. + + Parameters + ---------- + path : str + Path to the grid run folder + final_columns : dict + Dictionary mapping column types to file paths + + Returns + ------- + dict + Dictionary mapping column types to destination paths in the grid run folder + """ + print('Using the following configuration layers for columns:') + out_paths = {} + for key, value in final_columns.items(): + print(f" - {key}: {value}") + dest = os.path.join(path, 'column_lists', column_types[key]) + shutil.copy(value, dest) + out_paths[key] = dest + return out_paths + +def _copy_extras(path, final_extras): + """Copy extras files (makefiles, run files) to the grid run folder. + + Parameters + ---------- + path : str + Path to the grid run folder + final_extras : dict + Dictionary mapping extras keys to file paths + """ + print('Using the following configuration layers for extras:') + + # Define destination mapping for each extras key + extras_destinations = { + 'makefile_binary': [('binary/make', 'makefile_binary')], + 'makefile_star': [('star1/make', 'makefile_star'), + ('star2/make', 'makefile_star')], + 'binary_run': [('binary/src', 'binary_run.f')], + 'star_run': [('star1/src', 'run.f'), + ('star2/src', 'run.f')], + 'binary_extras': [('binary/src', 'run_binary_extras.f')], + 'star_binary_extras': [('binary/src', 'run_star_extras.f')], + 'star1_extras': [('star1/src', 'run_star_extras.f')], + 'star2_extras': [('star2/src', 'run_star_extras.f')], + } + + for key, value in final_extras.items(): + print(f" - {key}: {value}") + + if key == 'mesa_dir': + continue + + if key in extras_destinations: + for subdir, filename in extras_destinations[key]: + dest = os.path.join(path, subdir, filename) + shutil.copy(value, dest) + else: + print(f"Warning: Unrecognized extras key '{key}'. Copying to root.") + shutil.copy(value, path) + + +def setup_grid_run_folder(path, final_columns, final_extras, final_inlists, verbose=False): + """Set up the grid run folder by: + + 1. Creating necessary subdirectories + 2. Copying columns and extras into the right folders + 3. Writing the inlist files + 4. Building MESA executables + + Parameters + ---------- + path : str + Path to the grid run folder + final_columns : dict + Dictionary mapping column types to file paths + final_extras : dict + Dictionary mapping extras keys to file paths + final_inlists : dict + Dictionary mapping inlist keys to parameter dictionaries + verbose : bool, optional + If True, print additional information + """ + # Create directory structure + subdirs = ['binary', 'binary/make', 'binary/src', + 'star1', 'star1/make', 'star1/src', + 'star2', 'star2/make', 'star2/src', + 'column_lists'] + + for subdir in subdirs: + dir_path = os.path.join(path, subdir) + os.makedirs(dir_path, exist_ok=True) + + print(f"\nSetting up grid run folder at: {path}") + + # Copy columns and extras + column_paths = _copy_columns(path, final_columns) + _copy_extras(path, final_extras) + + # Create and run build script + _create_build_script(path) + + # Write inlist files + print('\nWriting MESA inlist files:') + inlist_binary_project = os.path.join(path, 'binary', 'inlist_project') + inlist_star1_binary = os.path.join(path, 'binary', 'inlist1') + inlist_star2_binary = os.path.join(path, 'binary', 'inlist2') + + # Add inlist names to binary_job section + final_inlists['binary_job']['inlist_names(1)'] = f"'{inlist_star1_binary}'" + final_inlists['binary_job']['inlist_names(2)'] = f"'{inlist_star2_binary}'" + + # Write all three inlist files + _write_binary_inlist(inlist_binary_project, + final_inlists['binary_controls'], + final_inlists['binary_job']) + + _write_star_inlist(inlist_star1_binary, + final_inlists['star1_controls'], + final_inlists['star1_job']) + + _write_star_inlist(inlist_star2_binary, + final_inlists['star2_controls'], + final_inlists['star2_job']) + + # Essentials paths created in this functions + output_paths = { + 'binary_executable': os.path.join(path, 'binary', 'binary'), + 'star1_executable': os.path.join(path, 'star1', 'star'), + 'star2_executable': os.path.join(path, 'star2', 'star'), + 'inlist_binary_project': inlist_binary_project, + 'inlist_star1_binary': inlist_star1_binary, + 'inlist_star2_binary': inlist_star2_binary, + } + output_paths.update(column_paths) + + return output_paths + + + +def _write_environment_setup(f, slurm): + """Write environment setup commands to a script file. + + Parameters + ---------- + f : file object + Open file to write to + slurm : dict + SLURM configuration dictionary + """ + f.write(f'export OMP_NUM_THREADS={slurm["number_of_cpus_per_task"]}\n\n') + f.write(f'export MESASDK_ROOT={os.environ["MESASDK_ROOT"]}\n') + f.write('source $MESASDK_ROOT/bin/mesasdk_init.sh\n') + f.write(f'export MESA_DIR={os.environ["MESA_DIR"]}\n\n') + + +def _write_sbatch_header(f, slurm, job_type='grid', array_size=None): + """Write SBATCH header directives to a script file. + + Parameters + ---------- + f : file object + Open file to write to + slurm : dict + SLURM configuration dictionary + job_type : str + Type of job ('grid', 'cleanup') + array_size : int or None + If provided, creates job array with this size + """ + f.write('#!/bin/bash\n') + f.write(f'#SBATCH --account={slurm["account"]}\n') + f.write(f'#SBATCH --partition={slurm["partition"]}\n') + + if job_type == 'cleanup': + f.write('#SBATCH -N 1\n') + f.write('#SBATCH --cpus-per-task 1\n') + f.write('#SBATCH --ntasks-per-node 1\n') + f.write(f'#SBATCH --time={slurm["walltime"]}\n') + f.write('#SBATCH --job-name="mesa_grid_cleanup"\n') + f.write('#SBATCH --output=mesa_cleanup.out\n') + elif array_size is not None: # Job array mode + f.write('#SBATCH -N 1\n') + f.write(f'#SBATCH --array=0-{array_size-1}\n') + f.write(f'#SBATCH --cpus-per-task {slurm["number_of_cpus_per_task"]}\n') + f.write('#SBATCH --ntasks-per-node 1\n') + f.write(f'#SBATCH --time={slurm["walltime"]}\n') + f.write('#SBATCH --job-name="mesa_grid_${SLURM_ARRAY_TASK_ID}"\n') + f.write('#SBATCH --output=mesa_grid.%A_%a.out\n') + else: # MPI mode + f.write(f'#SBATCH -N {slurm["number_of_nodes"]}\n') + f.write(f'#SBATCH --cpus-per-task {slurm["number_of_cpus_per_task"]}\n') + f.write(f'#SBATCH --ntasks-per-node {slurm["number_of_mpi_tasks"]}\n') + f.write(f'#SBATCH --time={slurm["walltime"]}\n') + f.write('#SBATCH --output="mesa_grid.out"\n') + + f.write('#SBATCH --mail-type=ALL\n') + f.write(f'#SBATCH --mail-user={slurm["email"]}\n') + f.write('#SBATCH --mem-per-cpu=4G\n\n') + + +def _write_cleanup_commands(f, slurm): + """Write cleanup commands to a script file. + + Parameters + ---------- + f : file object + Open file to write to + slurm : dict + SLURM configuration dictionary + """ + f.write('compress-mesa .\n') + if 'newgroup' in slurm: + f.write(f'echo "Change group to {slurm["newgroup"]}"\n') + f.write(f'chgrp -fR {slurm["newgroup"]} .\n') + f.write('echo "Change group permission to rwX at least"\n') + f.write('chmod -fR g+rwX .\n') + f.write('\necho "Done."') + + +def generate_submission_scripts(submission_type, command_line, slurm, nr_systems): + """Generate submission scripts (shell or SLURM) for running the grid. + + Parameters + ---------- + submission_type : str + Type of submission script to generate ('shell' or 'slurm') + command_line : str + The base command line to execute + slurm : dict + Dictionary containing SLURM configuration parameters + nr_systems : list + List of systems in the grid (used for job array length) + """ + if submission_type == 'shell': + script_name = 'grid_command.sh' + if os.path.exists(script_name): + Pwarn(f'Replace {script_name}', "OverwriteWarning") + + with open(script_name, 'w') as f: + f.write('#!/bin/bash\n\n') + _write_environment_setup(f, slurm) + + # Job array loop if needed + if slurm['job_array']: + indices = ' '.join(str(i) for i in range(nr_systems)) + f.write(f'for SLURM_ARRAY_TASK_ID in {indices}; do ') + + f.write(command_line) + + if slurm['job_array']: + f.write(' ; done\n\n') + else: + f.write('\n\n') + + _write_cleanup_commands(f, slurm) + + os.system(f"chmod 755 {script_name}") + print(f"Created {script_name}") + + elif submission_type == 'slurm': + # Generate main grid submission script + grid_script = 'job_array_grid_submit.slurm' if slurm['job_array'] else 'mpi_grid_submit.slurm' + if os.path.exists(grid_script): + Pwarn(f'Replace {grid_script}', "OverwriteWarning") + + array_size = nr_systems if slurm['job_array'] else None + with open(grid_script, 'w') as f: + _write_sbatch_header(f, slurm, job_type='grid', array_size=array_size) + _write_environment_setup(f, slurm) + f.write(command_line) + + # Generate cleanup script + cleanup_script = 'cleanup.slurm' + if os.path.exists(cleanup_script): + Pwarn(f'Replace {cleanup_script}', "OverwriteWarning") + + with open(cleanup_script, 'w') as f: + _write_sbatch_header(f, slurm, job_type='cleanup') + _write_cleanup_commands(f, slurm) + + # Generate wrapper run script + run_script = 'run_grid.sh' + if os.path.exists(run_script): + Pwarn(f'Replace {run_script}', "OverwriteWarning") + + with open(run_script, 'w') as f: + f.write('#!/bin/bash\n') + f.write(f'ID_GRID=$(sbatch --parsable {grid_script})\n') + f.write(f'echo "{grid_script} submitted as "${{ID_GRID}}\n') + f.write('ID_cleanup=$(sbatch --parsable --dependency=afterany:${ID_GRID} ' + '--kill-on-invalid-dep=yes cleanup.slurm)\n') + f.write('echo "cleanup.slurm submitted as "${ID_cleanup}\n') + + os.system(f"chmod 755 {run_script}") + print(f"Created {grid_script}, {cleanup_script}, and {run_script}") + + +def construct_command_line(number_of_mpi_processes, path_to_grid, + binary_exe, star1_exe, star2_exe, + inlist_binary_project, inlist_star1_binary, inlist_star2_binary, + inlist_star1_formation, inlist_star2_formation, + star_history_columns, binary_history_columns, profile_columns, + run_directory, grid_type, path_to_run_grid_exec, + psycris_inifile=None, keep_profiles=False, + keep_photos=False): + """Based on the inifile construct the command line call to posydon-run-grid + """ + if grid_type == "fixed": + command_line = 'python {15} --mesa-grid {1} --mesa-binary-executable {2} ' + elif grid_type == "dynamic": + command_line = 'mpirun --bind-to none -np {0} python -m mpi4py {15} --mesa-grid {1} --mesa-binary-executable {2} ' + else: + raise ValueError("grid_type can either be fixed or dynamic not anything else") + command_line += '--mesa-star1-executable {3} --mesa-star2-executable {4} --mesa-binary-inlist-project {5} ' + command_line += '--mesa-binary-inlist1 {6} --mesa-binary-inlist2 {7} --mesa-star1-inlist-project {8} ' + command_line += '--mesa-star2-inlist-project {9} --mesa-star-history-columns {10} ' + command_line += '--mesa-binary-history-columns {11} --mesa-profile-columns {12} ' + command_line += '--output-directory {13} --grid-type {14} ' + command_line += '--psycris-inifile {16}' + if keep_profiles: + command_line += ' --keep_profiles' + if keep_photos: + command_line += ' --keep_photos' + command_line = command_line.format(number_of_mpi_processes, + path_to_grid, + binary_exe, + star1_exe, + star2_exe, + inlist_binary_project, + inlist_star1_binary, + inlist_star2_binary, + inlist_star1_formation, + inlist_star2_formation, + star_history_columns, + binary_history_columns, + profile_columns, + run_directory, + grid_type, + path_to_run_grid_exec, + psycris_inifile) + return command_line diff --git a/posydon/unit_tests/CLI/grids/__init__.py b/posydon/unit_tests/CLI/grids/__init__.py new file mode 100644 index 0000000000..bae04d518b --- /dev/null +++ b/posydon/unit_tests/CLI/grids/__init__.py @@ -0,0 +1 @@ +# Unit tests for CLI/grids module diff --git a/posydon/unit_tests/CLI/grids/test_setup.py b/posydon/unit_tests/CLI/grids/test_setup.py new file mode 100644 index 0000000000..e921d5970e --- /dev/null +++ b/posydon/unit_tests/CLI/grids/test_setup.py @@ -0,0 +1,542 @@ +"""Unit tests of posydon/CLI/grids/setup.py""" + +__authors__ = [ + "GitHub Copilot " +] + +import os +import subprocess +from unittest.mock import MagicMock, call, patch + +import pytest + +# import the module which will be tested +import posydon.CLI.grids.setup as totest + + +class TestSetupMESADefaults: + """Test class for setup_MESA_defaults function.""" + + @pytest.fixture + def mock_mesa_dir(self, tmp_path): + """Create a mock MESA directory structure for testing.""" + mesa_dir = tmp_path / "mesa" + mesa_dir.mkdir() + + # Create binary work directory structure + binary_make = mesa_dir / "binary" / "work" / "make" + binary_make.mkdir(parents=True) + (binary_make / "makefile").write_text("# Binary makefile") + + binary_src = mesa_dir / "binary" / "work" / "src" + binary_src.mkdir(parents=True) + (binary_src / "run_binary.f").write_text("! Binary run file") + (binary_src / "run_binary_extras.f").write_text("! Binary extras") + (binary_src / "run_star_extras.f").write_text("! Star binary extras") + + # Create star work directory structure + star_make = mesa_dir / "star" / "work" / "make" + star_make.mkdir(parents=True) + (star_make / "makefile").write_text("# Star makefile") + + star_src = mesa_dir / "star" / "work" / "src" + star_src.mkdir(parents=True) + (star_src / "run.f").write_text("! Star run file") + (star_src / "run_star_extras.f").write_text("! Star extras") + + # Create defaults directory with column files + binary_defaults = mesa_dir / "binary" / "defaults" + binary_defaults.mkdir(parents=True) + (binary_defaults / "history_columns.list").write_text("# Binary history columns") + + star_defaults = mesa_dir / "star" / "defaults" + star_defaults.mkdir(parents=True) + (star_defaults / "history_columns.list").write_text("# Star history columns") + (star_defaults / "profile_columns.list").write_text("# Profile columns") + + return str(mesa_dir) + + @pytest.fixture + def mock_version_path(self, tmp_path): + """Create a mock version path.""" + version_path = tmp_path / "r11701" + version_path.mkdir() + + mesa_defaults = version_path / "MESA_defaults" + mesa_defaults.mkdir() + + return str(version_path) + + def test_setup_MESA_defaults_success(self, mock_mesa_dir, mock_version_path): + """Test successful setup of MESA defaults.""" + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + inlists, extras, columns = totest.setup_MESA_defaults(mock_version_path) + + # Check that all three dictionaries are returned + assert isinstance(inlists, dict) + assert isinstance(extras, dict) + assert isinstance(columns, dict) + + # Check extras dictionary has all expected keys + expected_extras_keys = [ + 'makefile_binary', 'makefile_star', 'star_run', 'binary_run', + 'binary_extras', 'star_binary_extras', 'star1_extras', 'star2_extras' + ] + for key in expected_extras_keys: + assert key in extras, f"Missing key: {key}" + assert extras[key], f"Empty path for key: {key}" + + # Check columns dictionary has all expected keys + expected_column_keys = [ + 'star_history_columns', 'binary_history_columns', 'profile_columns' + ] + for key in expected_column_keys: + assert key in columns, f"Missing key: {key}" + assert columns[key], f"Empty path for key: {key}" + + def test_setup_MESA_defaults_paths_correctness(self, mock_mesa_dir, mock_version_path): + """Test that returned paths are correctly formatted.""" + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + inlists, extras, columns = totest.setup_MESA_defaults(mock_version_path) + + # Check makefile paths + assert extras['makefile_binary'].endswith('binary/work/make/makefile') + assert extras['makefile_star'].endswith('star/work/make/makefile') + + # Check run file paths + assert extras['star_run'].endswith('star/work/src/run.f') + assert extras['binary_run'].endswith('binary/work/src/run_binary.f') + + # Check extras file paths + assert extras['binary_extras'].endswith('binary/work/src/run_binary_extras.f') + assert extras['star_binary_extras'].endswith('binary/work/src/run_star_extras.f') + assert extras['star1_extras'].endswith('star/work/src/run_star_extras.f') + assert extras['star2_extras'].endswith('star/work/src/run_star_extras.f') + + # Check that star1_extras and star2_extras point to the same file + assert extras['star1_extras'] == extras['star2_extras'] + + # Check column file paths + assert columns['star_history_columns'].endswith('star/defaults/history_columns.list') + assert columns['binary_history_columns'].endswith('binary/defaults/history_columns.list') + assert columns['profile_columns'].endswith('star/defaults/profile_columns.list') + + def test_setup_MESA_defaults_all_paths_contain_mesa_dir(self, mock_mesa_dir, mock_version_path): + """Test that all returned paths start with MESA_DIR.""" + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + inlists, extras, columns = totest.setup_MESA_defaults(mock_version_path) + + # Check all extras paths + for key, path in extras.items(): + assert path.startswith(mock_mesa_dir), f"{key} path doesn't start with MESA_DIR" + + # Check all column paths + for key, path in columns.items(): + assert path.startswith(mock_mesa_dir), f"{key} path doesn't start with MESA_DIR" + + def test_setup_MESA_defaults_missing_mesa_dir_env(self, mock_version_path): + """Test that function fails when MESA_DIR environment variable is not set.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(KeyError): + totest.setup_MESA_defaults(mock_version_path) + + def test_setup_MESA_defaults_missing_extras_file(self, mock_mesa_dir, mock_version_path): + """Test that function raises ValueError when an extras file is missing.""" + # Remove one of the required files + binary_run_path = os.path.join(mock_mesa_dir, "binary", "work", "src", "run_binary.f") + os.remove(binary_run_path) + + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + with pytest.raises(ValueError, match="does not exist"): + totest.setup_MESA_defaults(mock_version_path) + + def test_setup_MESA_defaults_missing_column_file(self, mock_mesa_dir, mock_version_path): + """Test that function raises ValueError when a column file is missing.""" + # Remove one of the required column files + profile_columns_path = os.path.join(mock_mesa_dir, "star", "defaults", "profile_columns.list") + os.remove(profile_columns_path) + + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + with pytest.raises(ValueError, match="does not exist"): + totest.setup_MESA_defaults(mock_version_path) + + def test_setup_MESA_defaults_empty_inlists_dict(self, mock_mesa_dir, mock_version_path): + """Test that inlists dictionary is empty (as per current implementation).""" + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + inlists, extras, columns = totest.setup_MESA_defaults(mock_version_path) + + # Current implementation returns empty inlists dict + assert inlists == {} + + def test_setup_MESA_defaults_file_existence_check(self, mock_mesa_dir, mock_version_path, capsys): + """Test that files are checked for existence and errors are printed.""" + # Create a scenario with a missing file + binary_extras_path = os.path.join(mock_mesa_dir, "binary", "work", "src", "run_binary_extras.f") + os.remove(binary_extras_path) + + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + with pytest.raises(ValueError): + totest.setup_MESA_defaults(mock_version_path) + + # Check that error message was printed + captured = capsys.readouterr() + assert "does not exist" in captured.out + + def test_setup_MESA_defaults_mesa_path_helper(self, mock_mesa_dir, mock_version_path): + """Test that the internal mesa_path helper function works correctly.""" + with patch.dict(os.environ, {'MESA_DIR': mock_mesa_dir}): + inlists, extras, columns = totest.setup_MESA_defaults(mock_version_path) + + # Verify that paths are constructed using proper path joining + # This indirectly tests the mesa_path helper function + expected_binary_run = os.path.join(mock_mesa_dir, 'binary', 'work', 'src', 'run_binary.f') + assert extras['binary_run'] == expected_binary_run + + def test_setup_MESA_defaults_with_special_characters_in_path(self, tmp_path): + """Test that function handles paths with special characters.""" + # Create a MESA directory with spaces in the path + mesa_dir = tmp_path / "mesa test dir" + mesa_dir.mkdir() + + # Create minimal required structure + for module in ['binary', 'star']: + work_dirs = ['make', 'src'] + for work_dir in work_dirs: + (mesa_dir / module / "work" / work_dir).mkdir(parents=True) + (mesa_dir / module / "defaults").mkdir(parents=True) + + # Create all required files + (mesa_dir / "binary" / "work" / "make" / "makefile").touch() + (mesa_dir / "star" / "work" / "make" / "makefile").touch() + (mesa_dir / "star" / "work" / "src" / "run.f").touch() + (mesa_dir / "binary" / "work" / "src" / "run_binary.f").touch() + (mesa_dir / "binary" / "work" / "src" / "run_binary_extras.f").touch() + (mesa_dir / "binary" / "work" / "src" / "run_star_extras.f").touch() + (mesa_dir / "star" / "work" / "src" / "run_star_extras.f").touch() + (mesa_dir / "binary" / "defaults" / "history_columns.list").touch() + (mesa_dir / "star" / "defaults" / "history_columns.list").touch() + (mesa_dir / "star" / "defaults" / "profile_columns.list").touch() + + version_path = tmp_path / "version dir" + version_path.mkdir() + + with patch.dict(os.environ, {'MESA_DIR': str(mesa_dir)}): + inlists, extras, columns = totest.setup_MESA_defaults(str(version_path)) + + # Should succeed without errors + assert len(extras) > 0 + assert len(columns) > 0 + + +class TestCheckFileExist: + """Test class for check_file_exist function.""" + + def test_check_file_exist_with_existing_file(self, tmp_path): + """Test that function returns True for existing file.""" + test_file = tmp_path / "test.txt" + test_file.write_text("test content") + + result = totest.check_file_exist(str(test_file)) + assert result is True + + def test_check_file_exist_with_missing_file_raise_error(self, tmp_path): + """Test that function raises ValueError for missing file when raise_error=True.""" + missing_file = tmp_path / "missing.txt" + + with pytest.raises(ValueError, match="does not exist"): + totest.check_file_exist(str(missing_file), raise_error=True) + + def test_check_file_exist_with_missing_file_no_raise(self, tmp_path): + """Test that function returns False for missing file when raise_error=False.""" + missing_file = tmp_path / "missing.txt" + + result = totest.check_file_exist(str(missing_file), raise_error=False) + assert result is False + + def test_check_file_exist_prints_error_message(self, tmp_path, capsys): + """Test that error message is printed when file doesn't exist.""" + missing_file = tmp_path / "missing.txt" + + with pytest.raises(ValueError): + totest.check_file_exist(str(missing_file)) + + captured = capsys.readouterr() + assert "does not exist" in captured.out + assert str(missing_file) in captured.out + + def test_check_file_exist_with_directory(self, tmp_path): + """Test that function works with directories (os.path.exists returns True for dirs).""" + test_dir = tmp_path / "test_dir" + test_dir.mkdir() + + result = totest.check_file_exist(str(test_dir)) + assert result is True + + +class TestSetupInlistRepository: + """Test class for setup_inlist_repository function.""" + + @pytest.fixture + def mock_subprocess_success(self): + """Mock subprocess.run to return successful results.""" + mock_result = MagicMock() + mock_result.stdout = "Success" + mock_result.returncode = 0 + return mock_result + + def test_setup_inlist_repository_new_directory(self, tmp_path, mock_subprocess_success, capsys): + """Test setup when inlist repository directory doesn't exist.""" + inlist_repo = tmp_path / "new_inlist_repo" + mesa_version = "r11701" + + # Create the version folder after the repo would be cloned + version_path = inlist_repo / mesa_version + + with patch('subprocess.run', return_value=mock_subprocess_success) as mock_run: + # Create the directory structure as git clone would + def side_effect(*args, **kwargs): + if 'clone' in args[0]: + inlist_repo.mkdir(parents=True, exist_ok=True) + version_path.mkdir(parents=True, exist_ok=True) + return mock_subprocess_success + + mock_run.side_effect = side_effect + + result = totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Check that directory was created + assert inlist_repo.exists() + + # Check that git clone was called + assert any('clone' in str(call_args) for call_args in mock_run.call_args_list) + + # Check that git pull was called + assert any('pull' in str(call_args) for call_args in mock_run.call_args_list) + + # Check return value + assert result == str(version_path) + + # Check printed messages + captured = capsys.readouterr() + assert "We are setting up your inlist repository now" in captured.out + assert "Creating inlist repository" in captured.out + + def test_setup_inlist_repository_existing_empty_directory(self, tmp_path, mock_subprocess_success, capsys): + """Test setup when directory exists but is empty.""" + inlist_repo = tmp_path / "empty_repo" + inlist_repo.mkdir() + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + + with patch('subprocess.run', return_value=mock_subprocess_success) as mock_run: + # Create version folder during clone + def side_effect(*args, **kwargs): + if 'clone' in args[0]: + version_path.mkdir(parents=True, exist_ok=True) + return mock_subprocess_success + + mock_run.side_effect = side_effect + + result = totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Check that git clone was called + clone_calls = [c for c in mock_run.call_args_list if 'clone' in str(c)] + assert len(clone_calls) == 1 + + # Verify the clone command + clone_call = clone_calls[0] + command_list = clone_call[0][0] + assert 'git' in command_list + assert 'clone' in command_list + # Check that the URL is in the command list + assert any('POSYDON-MESA-INLISTS.git' in arg for arg in command_list) + + assert result == str(version_path) + + def test_setup_inlist_repository_existing_nonempty_directory(self, tmp_path, mock_subprocess_success, capsys): + """Test setup when directory exists and contains files (repo already cloned).""" + inlist_repo = tmp_path / "existing_repo" + inlist_repo.mkdir() + + # Create some files to simulate existing repo + (inlist_repo / "README.md").write_text("# POSYDON MESA INLISTS") + + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + version_path.mkdir(parents=True) + + with patch('subprocess.run', return_value=mock_subprocess_success) as mock_run: + result = totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Check that git clone was NOT called + clone_calls = [c for c in mock_run.call_args_list if 'clone' in str(c)] + assert len(clone_calls) == 0 + + # Check that git pull was still called + pull_calls = [c for c in mock_run.call_args_list if 'pull' in str(c)] + assert len(pull_calls) == 1 + + # Check printed messages + captured = capsys.readouterr() + assert "Files found in inlist repository" in captured.out + assert "assuming POSYDON inlist repository is already cloned" in captured.out + + assert result == str(version_path) + + def test_setup_inlist_repository_git_pull_with_correct_cwd(self, tmp_path, mock_subprocess_success): + """Test that git pull is called with correct working directory.""" + inlist_repo = tmp_path / "repo" + inlist_repo.mkdir() + (inlist_repo / "README.md").write_text("test") + + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + version_path.mkdir(parents=True) + + with patch('subprocess.run', return_value=mock_subprocess_success) as mock_run: + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Find the git pull call + pull_calls = [c for c in mock_run.call_args_list if 'pull' in str(c)] + assert len(pull_calls) == 1 + + # Check that cwd parameter was set correctly + pull_call = pull_calls[0] + assert pull_call[1].get('cwd') == str(inlist_repo) + + def test_setup_inlist_repository_missing_version_folder(self, tmp_path, mock_subprocess_success): + """Test that ValueError is raised when MESA version folder doesn't exist.""" + inlist_repo = tmp_path / "repo" + inlist_repo.mkdir() + (inlist_repo / "README.md").write_text("test") + + mesa_version = "r99999" # Non-existent version + + with patch('subprocess.run', return_value=mock_subprocess_success): + with pytest.raises(ValueError, match="does not exist in the inlist repository"): + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + def test_setup_inlist_repository_git_clone_failure(self, tmp_path): + """Test handling of git clone failure.""" + inlist_repo = tmp_path / "repo" + mesa_version = "r11701" + + # Mock subprocess to raise CalledProcessError for git clone + with patch('subprocess.run') as mock_run: + mock_run.side_effect = subprocess.CalledProcessError(1, 'git clone') + + with pytest.raises(subprocess.CalledProcessError): + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + def test_setup_inlist_repository_git_pull_failure(self, tmp_path): + """Test handling of git pull failure.""" + inlist_repo = tmp_path / "repo" + inlist_repo.mkdir() + (inlist_repo / "README.md").write_text("test") + + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + version_path.mkdir(parents=True) + + # Mock subprocess to fail on git pull + with patch('subprocess.run') as mock_run: + mock_run.side_effect = subprocess.CalledProcessError(1, 'git pull') + + with pytest.raises(subprocess.CalledProcessError): + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + def test_setup_inlist_repository_prints_git_output(self, tmp_path, capsys): + """Test that git command output is printed.""" + inlist_repo = tmp_path / "repo" + inlist_repo.mkdir() + (inlist_repo / "README.md").write_text("test") + + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + version_path.mkdir(parents=True) + + mock_result = MagicMock() + mock_result.stdout = "Already up to date." + mock_result.returncode = 0 + + with patch('subprocess.run', return_value=mock_result): + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + captured = capsys.readouterr() + assert "Already up to date." in captured.out + assert "Updating inlist repository" in captured.out + + def test_setup_inlist_repository_subprocess_parameters(self, tmp_path, mock_subprocess_success): + """Test that subprocess.run is called with correct parameters.""" + inlist_repo = tmp_path / "repo" + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + + with patch('subprocess.run', return_value=mock_subprocess_success) as mock_run: + # Create directories during clone simulation + def side_effect(*args, **kwargs): + if 'clone' in args[0]: + inlist_repo.mkdir(parents=True, exist_ok=True) + version_path.mkdir(parents=True, exist_ok=True) + return mock_subprocess_success + + mock_run.side_effect = side_effect + + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Check all subprocess calls have correct parameters + for call_obj in mock_run.call_args_list: + # Check that capture_output, text, and check parameters are set + assert call_obj[1].get('capture_output') is True + assert call_obj[1].get('text') is True + assert call_obj[1].get('check') is True + + def test_setup_inlist_repository_correct_git_url(self, tmp_path, mock_subprocess_success): + """Test that correct POSYDON MESA INLISTS URL is used.""" + inlist_repo = tmp_path / "repo" + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + + with patch('subprocess.run', return_value=mock_subprocess_success) as mock_run: + # Create directories during clone simulation + def side_effect(*args, **kwargs): + if 'clone' in args[0]: + inlist_repo.mkdir(parents=True, exist_ok=True) + version_path.mkdir(parents=True, exist_ok=True) + return mock_subprocess_success + + mock_run.side_effect = side_effect + + totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Find the clone call and verify URL + clone_calls = [c for c in mock_run.call_args_list if 'clone' in str(c)] + if clone_calls: + clone_call = clone_calls[0] + command = clone_call[0][0] + assert 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git' in command + + def test_setup_inlist_repository_return_value_format(self, tmp_path, mock_subprocess_success): + """Test that return value is correctly formatted path.""" + inlist_repo = tmp_path / "repo" + inlist_repo.mkdir() + (inlist_repo / "README.md").write_text("test") + + mesa_version = "r11701" + version_path = inlist_repo / mesa_version + version_path.mkdir(parents=True) + + with patch('subprocess.run', return_value=mock_subprocess_success): + result = totest.setup_inlist_repository(str(inlist_repo), mesa_version) + + # Check that result is a string path + assert isinstance(result, str) + + # Check that it ends with the MESA version + assert result.endswith(mesa_version) + + # Check that it contains the inlist_repo path + assert str(inlist_repo) in result + + # Check that the path exists + assert os.path.exists(result) From 328e9075cd0d545186f64cdd51a8e1532dc73bf8 Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 23 Jan 2026 21:46:53 +0100 Subject: [PATCH 02/13] add debugging levels --- bin/posydon-setup-grid | 43 ++--- posydon/CLI/grids/setup.py | 323 +++++++++++++++++++++++++------------ 2 files changed, 246 insertions(+), 120 deletions(-) diff --git a/bin/posydon-setup-grid b/bin/posydon-setup-grid index dfdcfe7524..31550dca9a 100755 --- a/bin/posydon-setup-grid +++ b/bin/posydon-setup-grid @@ -4,6 +4,7 @@ ############################################################################## import argparse import glob +import logging import os import shutil import subprocess @@ -21,6 +22,7 @@ from posydon.CLI.grids.setup import ( resolve_inlists, setup_grid_run_folder, setup_inlist_repository, + setup_logger, setup_MESA_defaults, setup_POSYDON, setup_user, @@ -30,6 +32,8 @@ from posydon.utils import configfile from posydon.utils import gridutils as utils from posydon.utils.posydonwarning import Pwarn +# Setup logger +logger = logging.getLogger(__name__) ############################################################################### # DEFINE COMMANDLINE ARGUMENTS @@ -54,8 +58,8 @@ def parse_commandline(): default='shell') parser.add_argument("-n", "--nproc", help="number of processors", type=int, default=1) - parser.add_argument("--verbose", action="store_true", default=False, - help="Run in Verbose Mode") + parser.add_argument("-v", "--verbose", nargs="?", const="console", default=None, + help="Enable verbose logging. Optionally specify a filename to log to (e.g., -v logfile.txt). If no filename provided, logs to console.") args = parser.parse_args() @@ -963,13 +967,18 @@ if __name__ == '__main__': ########################################################################### args = parse_commandline() - verbose = args.verbose if args.verbose else False + # Setup logging based on verbosity level + setup_logger(args.verbose) try: os.environ['MESA_DIR'] except: raise ValueError("MESA_DIR must be defined in your environment " "before you can run a grid of MESA runs") + # check if given file exists + if not os.path.isfile(args.inifile): + raise FileNotFoundError("The provided inifile does not exist, please check the path and try again") + # Determine location of executables proc = subprocess.Popen(['which', 'posydon-run-grid'], stdin = subprocess.PIPE, @@ -1000,19 +1009,16 @@ if __name__ == '__main__': raise ValueError("Please add psycris inifile to the [run_parameters] section of the inifile.") # Check if a base is provided for the run - if 'base' in user_mesa_inlists.keys() and user_mesa_inlists['base'] is not None and user_mesa_inlists['base'] != "": - print('base provided in inifile, copying base to run directory') - # check if inlist_repository is provided - if 'inlist_repository' not in user_mesa_inlists.keys(): - print("No inlist_repository provided in inifile") - print('Using your home folder as the inlist repository') - user_mesa_inlists['inlist_repository'] = os.path.expanduser('~') - else: + if ('base' not in user_mesa_inlists.keys() + or user_mesa_inlists['base'] is None + or user_mesa_inlists['base'] == ""): raise ValueError("Please provide a base for the MESA run in the configuration file") + logger.debug(f'Base provided in inifile:\n{user_mesa_inlists["base"]}') + # setup the inlist repository # MESA_version_base_path is the path to the base of the version in the inlist repository - MESA_version_root_path = setup_inlist_repository(user_mesa_inlists['inlist_repository'], + MESA_version_root_path = setup_inlist_repository(user_mesa_inlists.get('inlist_repository', None), user_mesa_inlists['mesa_version']) # Setup the MESA_default, which is always needed @@ -1054,16 +1060,14 @@ if __name__ == '__main__': user_inlists, grid_parameters=grid_parameters, output_settings=user_output_settings, - system_type=user_mesa_inlists['system_type'], - verbose=verbose) + system_type=user_mesa_inlists['system_type']) # Setup the run directory with all necessary files # This also creates the inlists for the grid run output_paths = setup_grid_run_folder(args.run_directory, final_columns, final_extras, - final_inlists, - verbose=verbose) + final_inlists) # Now we can create the mpi command line to run the grid if slurm['job_array']: @@ -1149,7 +1153,8 @@ if __name__ == '__main__': # Generate submission scripts generate_submission_scripts(args.submission_type, command_line, slurm, nr_systems) - print("Setup complete! You can now submit your grid to the cluster.") - print("To submit, run the following command:") + logger.info("Setup complete! You can now submit your grid to the cluster.") + if args.submission_type == 'slurm': - print(f" sbatch submit_slurm.sh") + logger.info("To submit, run the following command:") + logger.info(f" sbatch submit_slurm.sh") diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index 2aee43f408..5c840c3c4f 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -1,5 +1,6 @@ import argparse import glob +import logging import os import shutil import subprocess @@ -33,9 +34,92 @@ CYAN = '\033[96m' YELLOW = '\033[93m' MAGENTA = '\033[95m' +RED = '\033[91m' RESET = '\033[0m' BOLD = '\033[1m' +# Setup logger +logger = logging.getLogger(__name__) + +class ColoredFormatter(logging.Formatter): + """Custom formatter that adds colors to log level names.""" + + LEVEL_COLORS = { + 'DEBUG': GRAY, + 'INFO': CYAN, + 'WARNING': YELLOW, + 'ERROR': RED, + 'CRITICAL': RED + } + + def format(self, record): + # Get the color for this log level + color = self.LEVEL_COLORS.get(record.levelname, RESET) + + # Create the formatted message with colored level name + formatted = f'[{color}{record.levelname}{RESET}] {record.getMessage()}' + return formatted + +def setup_logger(verbose=None): + """Setup logging configuration based on verbosity level. + + Parameters + ---------- + verbose : str or None, optional + Verbose logging option: + None = No verbose logging (INFO level to console only) + "console" = DEBUG level output to console + filename = DEBUG level output to specified file + Default is None. + """ + if verbose is None: + level = logging.INFO + # Only setup console handler for INFO level + handler = logging.StreamHandler() + handler.setFormatter(ColoredFormatter()) + + # Configure the root logger + logging.basicConfig( + level=level, + handlers=[handler], + force=True + ) + elif verbose == "console": + level = logging.DEBUG + print(f"{CYAN}DEBUG verbosity enabled: Detailed output will be shown on console.{RESET}") + + # Create a custom handler with colored formatter + handler = logging.StreamHandler() + handler.setFormatter(ColoredFormatter()) + + # Configure the root logger + logging.basicConfig( + level=level, + handlers=[handler], + force=True + ) + else: + # verbose is a filename + level = logging.DEBUG + print(f"{CYAN}DEBUG verbosity enabled: Detailed output will be logged to {verbose}.{RESET}") + + # Create both file and console handlers + file_handler = logging.FileHandler(verbose) + console_handler = logging.StreamHandler() + + # Use colored formatter for console, plain formatter for file + console_handler.setFormatter(ColoredFormatter()) + file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M')) + + # Configure the root logger + logging.basicConfig( + level=level, + handlers=[file_handler, console_handler], + force=True + ) + + def check_file_exist(file_path, raise_error=True): """Check if a file exists at the given path @@ -74,39 +158,47 @@ def setup_inlist_repository(inlist_repository, MESA_version): Path to the base to use for the run """ POSYDON_inlist_URL = 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git' - print("We are setting up your inlist repository now") + logger.info("Loading repository for inlists.") + + if inlist_repository is None: + # check if inlist_repository is provided + logger.info("No inlist_repository provided in inifile.") + logger.info('Using your home folder as the inlist repository') + inlist_repository = os.path.expanduser('~') # check if the inlist repository path exists if not os.path.exists(inlist_repository): - print(f"Creating inlist repository at {inlist_repository}") + logger.debug(f"Creating inlist repository at {inlist_repository}") os.makedirs(inlist_repository) # check if it contains anything and if not, clone the repo if os.listdir(inlist_repository): - print(os.listdir(inlist_repository)) - print("Files found in inlist repository, assuming POSYDON inlist repository is already cloned!") + logger.debug(f"Files found in inlist repository: {os.listdir(inlist_repository)}") + logger.info("Assuming the POSYDON inlist repository is already cloned into this folder!") else: out = subprocess.run(['git', 'clone', POSYDON_inlist_URL, inlist_repository], capture_output=True, text=True, check=True,) - print(out.stdout) + if out.stderr: + logger.error(out.stderr) + else: + logger.debug("Cloned the POSYDON inlist repository successfully.") # update the repository - print("Updating inlist repository") + logger.debug("Updating the inlist repository to the latest version.") # TODO: Re-enable git pull after testing - print("Currently disabled for testing purposes") + logger.debug("Currently disabled for testing purposes") # out = subprocess.run(['git', 'pull'], # cwd=inlist_repository, # capture_output=True, # text=True, # check=True,) - # print(out.stdout) # check if the base is available as a folder in the repository version_root_path = os.path.join(inlist_repository, MESA_version) if not os.path.exists(version_root_path): - print(version_root_path) + logger.error(version_root_path) raise ValueError("The provided MESA version does not exist in the inlist repository, please check your provided MESA version and try again.") return version_root_path @@ -130,7 +222,7 @@ def setup_MESA_defaults(path_to_version): Dictionary of MESA default column files paths """ MESA_DIR = os.environ['MESA_DIR'] - + logger.debug(f"Setting up MESA defaults from MESA_DIR: {MESA_DIR}") #---------------------------------- # Inlists #---------------------------------- @@ -261,6 +353,8 @@ def setup_POSYDON(path_to_version, base, system_type): POSYDON_path = os.path.join(path_to_version, base) check_file_exist(POSYDON_path) + logger.debug(f"Setting up POSYDON configuration: {POSYDON_path}") + #---------------------------------- # Inlists #---------------------------------- @@ -415,7 +509,7 @@ def setup_user(user_mesa_inlists, user_mesa_extras): return user_inlists, user_extras, user_columns -def resolve_configuration(keys, MESA_defaults, POSYDON_config, user_config, verbose=False): +def resolve_configuration(keys, MESA_defaults, POSYDON_config, user_config, title="Configuration Priority"): """Resolve final configuration to use based on priority: user_config > POSYDON_config > MESA_defaults @@ -429,17 +523,14 @@ def resolve_configuration(keys, MESA_defaults, POSYDON_config, user_config, verb Dictionary of POSYDON configuration paths user_config : dict Dictionary of user configuration paths - verbose : bool, optional - If True, print a visual priority table. Default is False. + title : str, optional + Title for logging output Returns ------- final_config : dict Dictionary of final configuration paths to use """ - if verbose: - print_priority_table(keys, MESA_defaults, POSYDON_config, user_config) - final_config = {} for key in keys: @@ -450,10 +541,14 @@ def resolve_configuration(keys, MESA_defaults, POSYDON_config, user_config, verb else: final_config[key] = MESA_defaults.get(key) + # Log at DEBUG level with detailed table + if logger.isEnabledFor(logging.DEBUG): + print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, final_config, title) + return final_config -def resolve_columns(MESA_default_columns, POSYDON_columns, user_columns, verbose=False): +def resolve_columns(MESA_default_columns, POSYDON_columns, user_columns): """Resolve final columns to use based on priority: user_columns > POSYDON_columns > MESA_default_columns @@ -465,23 +560,30 @@ def resolve_columns(MESA_default_columns, POSYDON_columns, user_columns, verbose Dictionary of POSYDON column files paths user_columns : dict Dictionary of user column files paths - verbose : bool, optional - If True, print a visual priority table. Default is False. Returns ------- final_columns : dict Dictionary of final column files paths to use """ - if verbose: - print_priority_table(column_types, MESA_default_columns, - POSYDON_columns, user_columns, - title="Column Files Priority") - return resolve_configuration(column_types, MESA_default_columns, - POSYDON_columns, user_columns, verbose=False) + final_columns = resolve_configuration(column_types, MESA_default_columns, + POSYDON_columns, user_columns, + title="Column Files Priority") + # Log at INFO level which layer is used + logger.info(f"{BOLD}Column Files:{RESET}") + for name, filename in column_types.items(): + if user_columns.get(name) is not None: + logger.info(f" {name}: {MAGENTA}user{RESET}") + elif POSYDON_columns.get(name) is not None: + logger.info(f" {name}: {YELLOW}POSYDON{RESET}") + else: + logger.info(f" {name}: {CYAN}MESA{RESET}") + + return final_columns -def resolve_extras(MESA_default_extras, POSYDON_extras, user_extras, verbose=False): + +def resolve_extras(MESA_default_extras, POSYDON_extras, user_extras): """Resolve final extras to use based on priority: user_extras > POSYDON_extras > MESA_default_extras @@ -493,24 +595,31 @@ def resolve_extras(MESA_default_extras, POSYDON_extras, user_extras, verbose=Fal Dictionary of POSYDON extras paths user_extras : dict Dictionary of user extras paths - verbose : bool, optional - If True, print a visual priority table. Default is False. Returns ------- final_extras : dict Dictionary of final extras paths to use """ - if verbose: - print_priority_table(extras_keys, MESA_default_extras, - POSYDON_extras, user_extras, - title="EXTRAS Files Priority") - return resolve_configuration(extras_keys, MESA_default_extras, - POSYDON_extras, user_extras, verbose=False) + final_extras = resolve_configuration(extras_keys, MESA_default_extras, + POSYDON_extras, user_extras, + title="EXTRAS Files Priority") + # Log at INFO level which layer is used + logger.info(f"{BOLD}EXTRAS Files:{RESET}") + for key in extras_keys: + if user_extras.get(key) is not None: + logger.info(f" {key}: {MAGENTA}user{RESET}") + elif POSYDON_extras.get(key) is not None: + logger.info(f" {key}: {YELLOW}POSYDON{RESET}") + else: + logger.info(f" {key}: {CYAN}MESA{RESET}") -def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, title="Configuration Priority"): - """Print a visual table showing which configuration layer is used for each key. + return final_extras + + +def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, final_config, title="Configuration Priority"): + """Log a visual table showing which configuration layer is used for each key. Parameters ---------- @@ -522,6 +631,8 @@ def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, title Dictionary of POSYDON configuration paths user_config : dict Dictionary of user configuration paths + final_config : dict + Dictionary of final resolved configuration paths title : str, optional Title for the table """ @@ -531,13 +642,13 @@ def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, title col_width = 12 # Print title - print(f"\n{BOLD}{title}{RESET}") - print("=" * (max_key_len + col_width * 3 + 4)) + logger.debug(f"{BOLD}{title}{RESET}") + logger.debug("=" * (max_key_len + col_width * 3 + 4)) # Print header with colored column names header = f"{'Key':<{max_key_len}} {CYAN}{'MESA':^{col_width}}{RESET}{YELLOW}{'POSYDON':^{col_width}}{RESET}{MAGENTA}{'user':^{col_width}}{RESET}" - print(f"{BOLD}{header}{RESET}") - print("-" * (max_key_len + col_width * 3 + 4)) + logger.debug(f"{BOLD}{header}{RESET}") + logger.debug("-" * (max_key_len + col_width * 3 + 4)) # Print each row for key in keys: @@ -572,11 +683,11 @@ def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, title posydon_str = f"{GRAY}{posydon_mark}{RESET}" user_str = f"{GRAY}{user_mark}{RESET}" - # Print row - print(f"{key:<{max_key_len}} {mesa_str:^{col_width+9}}{posydon_str:^{col_width+9}}{user_str:^{col_width+9}}") + # Log row + logger.debug(f"{key:<{max_key_len}} {mesa_str:^{col_width+9}}{posydon_str:^{col_width+9}}{user_str:^{col_width+9}}") - print("=" * (max_key_len + col_width * 3 + 4)) - print(f"{GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used\n") + logger.debug("=" * (max_key_len + col_width * 3 + 4)) + logger.debug(f"{GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used.") def print_inlist_stacking_table(keys, MESA_defaults, POSYDON_config, user_config, title="Inlist Stacking"): """Print a visual table showing how inlists are stacked for each key. @@ -602,7 +713,7 @@ def print_inlist_stacking_table(keys, MESA_defaults, POSYDON_config, user_config max_key_len = max(len(str(key)) for key in keys) # Print title - print(f"\n{BOLD}{title}{RESET}") + print(f"{BOLD}{title}{RESET}") print("=" * 80) print(f"{BOLD}{'Key':<{max_key_len}} Layer Stack{RESET}") print("-" * 80) @@ -645,7 +756,7 @@ def print_inlist_stacking_table(keys, MESA_defaults, POSYDON_config, user_config print("=" * 80) print(f"Note: Files at the top override parameters from files below") - print(f"{MAGENTA}user{RESET} (highest priority) → {YELLOW}POSYDON{RESET} (config) → {CYAN}MESA{RESET} (base)\n") + print(f"{MAGENTA}user{RESET} (highest priority) → {YELLOW}POSYDON{RESET} (config) → {CYAN}MESA{RESET} (base)") def print_inlist_parameter_override_table(key, mesa_params, posydon_params, user_params, final_params, show_details=False): """Print a table showing which layer each parameter comes from, similar to extras/columns tables. @@ -685,7 +796,7 @@ def print_inlist_parameter_override_table(key, mesa_params, posydon_params, user # Print summary header overridden_count = sum(1 for p in all_params if (p in user_params and (p in mesa_params or p in posydon_params)) or (p in posydon_params and p in mesa_params)) - print(f"\n {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") + print(f" {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") print(" " + "=" * (max_param_len + col_width * 3 + 4)) # Print header with colored column names @@ -734,7 +845,7 @@ def print_inlist_parameter_override_table(key, mesa_params, posydon_params, user def print_inlist_parameter_override_table_v2(key, layer_params, final_params, show_details=False): - """Print a table showing which layer each parameter comes from (supports all layers). + """Log a table showing which layer each parameter comes from (supports all layers). Parameters ---------- @@ -799,21 +910,21 @@ def print_inlist_parameter_override_table_v2(key, layer_params, final_params, sh if count > 1: overridden_count += 1 - # Print summary header - print(f"\n {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") + # Log summary header + logger.debug(f" {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") total_width = max_param_len + col_width * len(active_layers) + len(active_layers) * 2 - print(" " + "=" * total_width) + logger.debug(" " + "=" * total_width) - # Print header with colored column names + # Log header with colored column names header_parts = [f"{'Parameter':<{max_param_len}}"] for layer_name in active_layers: color = layer_colors.get(layer_name, RESET) header_parts.append(f"{color}{layer_name:^{col_width}}{RESET}") header = " ".join(header_parts) - print(f" {BOLD}{header}{RESET}") - print(" " + "-" * total_width) + logger.debug(f" {BOLD}{header}{RESET}") + logger.debug(" " + "-" * total_width) - # Print each parameter row + # Log each parameter row for param in all_params: # Determine which layer provides the final value (check in priority order: highest to lowest) # Priority: output > grid > user > POSYDON > MESA @@ -837,18 +948,19 @@ def print_inlist_parameter_override_table_v2(key, layer_params, final_params, sh else: row_parts.append(f"{mark:^{col_width}}") - print(" " + " ".join(row_parts)) + logger.debug(" " + " ".join(row_parts)) - print(" " + "=" * total_width) - print(f" {GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used") + logger.debug(" " + "=" * total_width) + logger.debug(f" {GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used") def print_inlist_summary_table_v2(all_keys, layer_counts): - """Print a summary table showing parameter counts per section at each layer. + """Log a summary table showing parameter counts per section at each layer. This version supports multiple layers including grid and output configurations. + Logs at INFO level. Parameters ---------- @@ -877,10 +989,10 @@ def print_inlist_summary_table_v2(all_keys, layer_counts): if layer in layer_counts and any(layer_counts[layer].values())] num_layers = len(active_layers) - total_width = max_key_len + col_width * num_layers + (num_layers + 1) + total_width = max_key_len + col_width * num_layers + (num_layers + 4) - print(f"\n{BOLD}Parameter Count Summary{RESET}") - print("=" * total_width) + logger.info(f"{BOLD}Parameter Count Summary{RESET}") + logger.info("=" * total_width) # Print header with colored column names header_parts = [f"{'Section':<{max_key_len}}"] @@ -888,8 +1000,8 @@ def print_inlist_summary_table_v2(all_keys, layer_counts): color = layer_colors.get(layer, RESET) header_parts.append(f"{color}{layer:^{col_width}}{RESET}") header = " ".join(header_parts) - print(f"{BOLD}{header}{RESET}") - print("-" * total_width) + logger.info(f"{BOLD}{header}{RESET}") + logger.info("-" * total_width) # Print each section row for key in all_keys: @@ -898,10 +1010,10 @@ def print_inlist_summary_table_v2(all_keys, layer_counts): count = layer_counts[layer].get(key, 0) color = layer_colors.get(layer, RESET) row_parts.append(f"{color}{count:^{col_width}}{RESET}") - print(" ".join(row_parts)) + logger.info(" ".join(row_parts)) - print("=" * total_width) - print(f"\nLayer priority (lowest → highest): {' → '.join(active_layers)}\n") + logger.info("=" * total_width) + logger.info(f"Layer priority (lowest → highest): {' → '.join(active_layers)}") def _get_section_from_key(key): """Determine the MESA inlist section based on the key name. @@ -1028,12 +1140,15 @@ def _build_grid_parameter_layer(grid_parameters, final_inlists): read_param: '.true.', name_param: f"'{filename}'" } + # Log at DEBUG level which sections are affected by grid parameters + logger.debug(f" Grid parameters affecting {section}: {', '.join(matching_params)}") else: grid_layer[section] = {} - # - print('Adding grid parameters to sections:', - ', '.join([sec for sec, params in grid_layer.items() if params])) + # Log at DEBUG level summary + affected_sections = [sec for sec, params in grid_layer.items() if params] + if affected_sections: + logger.debug(f"Grid parameters affect sections: {', '.join(affected_sections)}") return grid_layer @@ -1099,9 +1214,15 @@ def to_fortran_bool(value): if 'binary_history' in output_settings and not output_settings['binary_history']: output_layer['binary_controls']['history_interval'] = "-1" - - print('Adding output control parameters to sections:', - ', '.join([sec for sec, params in output_layer.items() if params])) + # Log at DEBUG level which sections have output control parameters + affected_sections = [sec for sec, params in output_layer.items() if params] + if affected_sections: + logger.debug(f"Output control parameters affect sections: {', '.join(affected_sections)}") + for section in affected_sections: + params = output_layer[section] + if params: + param_list = ', '.join(params.keys()) + logger.debug(f" {section}: {param_list}") # Handle ZAMS filenames if provided if 'zams_filename_1' in output_settings and output_settings['zams_filename_1'] is not None: @@ -1114,7 +1235,7 @@ def to_fortran_bool(value): def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_type, - grid_parameters=None, output_settings=None, verbose=False, show_details=False): + grid_parameters=None, output_settings=None): """Resolve final inlists to use based on priority: output_settings > grid_parameters > user_inlists > POSYDON_inlists > MESA_default_inlists @@ -1146,10 +1267,6 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ Collection of grid parameter names. If provided, adds grid configuration layer. output_settings : dict, optional Dictionary of output settings. If provided, adds output control layer. - verbose : bool, optional - If True, print visual stacking and parameter count summary. Default is False. - show_details : bool, optional - If True, print detailed parameter-by-parameter tables. Default is False. Returns ------- @@ -1239,12 +1356,15 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ layer_counts['output'][key] = 0 layer_params['output'][key] = {} + # Log at INFO level: Parameter count summary + print_inlist_summary_table_v2(all_keys, layer_counts) - if show_details: + # Log at DEBUG level: Detailed parameter tables + if logger.isEnabledFor(logging.DEBUG): for key in all_keys: # Only show sections that have parameters in any layer if any(layer_params[layer][key] for layer in layer_params): - print(f"\n{BOLD}═══ {key} ═══{RESET}") + logger.debug(f"{BOLD}═══ {key} ═══{RESET}") print_inlist_parameter_override_table_v2( key, layer_params, @@ -1252,10 +1372,6 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ show_details=True ) - - if verbose: - print_inlist_summary_table_v2(all_keys, layer_counts) - # Return the final parameter dictionaries return final_inlists @@ -1373,7 +1489,7 @@ def _write_binary_inlist(filepath, binary_controls, binary_job): _write_inlist_section(f, 'binary_controls', binary_controls) f.write(b'\n') _write_inlist_section(f, 'binary_job', binary_job) - print(f'Wrote inlist: {filepath}') + logger.debug(f'Wrote inlist: {filepath}') def _write_star_inlist(filepath, star_controls, star_job): @@ -1392,7 +1508,7 @@ def _write_star_inlist(filepath, star_controls, star_job): _write_inlist_section(f, 'controls', star_controls) f.write(b'\n') _write_inlist_section(f, 'star_job', star_job) - print(f'Wrote inlist: {filepath}') + logger.debug(f'Wrote inlist: {filepath}') def _create_build_script(path): @@ -1405,7 +1521,7 @@ def _create_build_script(path): """ mk_filepath = os.path.join(path, 'mk') if os.path.exists(mk_filepath): - print(f"Warning: 'mk' file already exists. It will be overwritten.") + logger.warning(f"'mk' file already exists at {mk_filepath}. It will be overwritten.") with open(mk_filepath, 'w') as f: f.write(f'cd {os.path.join(path, "binary/make")}\n') @@ -1415,9 +1531,13 @@ def _create_build_script(path): f.write(f'cd {os.path.join(path, "star2/make")}\n') f.write(f'make -f makefile_star\n') - print(f'Created build script: {mk_filepath}') + logger.debug(f'Created build script: {mk_filepath}') subprocess.run(['chmod', '755', mk_filepath]) - subprocess.run(['./mk'], shell=True, cwd=path) + out = subprocess.run(['./mk'], shell=True, cwd=path, capture_output=True, text=True) + if out.returncode != 0: + logger.error(f"Building the MESA executables has failed!") + for line in out.stderr.strip().split('\n'): + logger.error(line) def _copy_columns(path, final_columns): """Copy column list files to the grid run folder. @@ -1434,10 +1554,11 @@ def _copy_columns(path, final_columns): dict Dictionary mapping column types to destination paths in the grid run folder """ - print('Using the following configuration layers for columns:') + + logger.debug(f'{BOLD}COLUMN LISTS USED:{RESET}') out_paths = {} for key, value in final_columns.items(): - print(f" - {key}: {value}") + logger.debug(f"{key}: {value}") dest = os.path.join(path, 'column_lists', column_types[key]) shutil.copy(value, dest) out_paths[key] = dest @@ -1453,7 +1574,7 @@ def _copy_extras(path, final_extras): final_extras : dict Dictionary mapping extras keys to file paths """ - print('Using the following configuration layers for extras:') + logger.debug(f'{BOLD}EXTRAS USED:{RESET}') # Define destination mapping for each extras key extras_destinations = { @@ -1470,7 +1591,7 @@ def _copy_extras(path, final_extras): } for key, value in final_extras.items(): - print(f" - {key}: {value}") + logger.debug(f"{key}: {value}") if key == 'mesa_dir': continue @@ -1480,11 +1601,11 @@ def _copy_extras(path, final_extras): dest = os.path.join(path, subdir, filename) shutil.copy(value, dest) else: - print(f"Warning: Unrecognized extras key '{key}'. Copying to root.") + logger.warning(f"Unrecognized extras key '{key}'. Copying to root.") shutil.copy(value, path) -def setup_grid_run_folder(path, final_columns, final_extras, final_inlists, verbose=False): +def setup_grid_run_folder(path, final_columns, final_extras, final_inlists): """Set up the grid run folder by: 1. Creating necessary subdirectories @@ -1502,8 +1623,6 @@ def setup_grid_run_folder(path, final_columns, final_extras, final_inlists, verb Dictionary mapping extras keys to file paths final_inlists : dict Dictionary mapping inlist keys to parameter dictionaries - verbose : bool, optional - If True, print additional information """ # Create directory structure subdirs = ['binary', 'binary/make', 'binary/src', @@ -1515,7 +1634,7 @@ def setup_grid_run_folder(path, final_columns, final_extras, final_inlists, verb dir_path = os.path.join(path, subdir) os.makedirs(dir_path, exist_ok=True) - print(f"\nSetting up grid run folder at: {path}") + logger.debug(f"Setting up grid run folder at: {path}") # Copy columns and extras column_paths = _copy_columns(path, final_columns) @@ -1525,7 +1644,7 @@ def setup_grid_run_folder(path, final_columns, final_extras, final_inlists, verb _create_build_script(path) # Write inlist files - print('\nWriting MESA inlist files:') + logger.debug(f'{BOLD}Writing MESA inlist files:{RESET}') inlist_binary_project = os.path.join(path, 'binary', 'inlist_project') inlist_star1_binary = os.path.join(path, 'binary', 'inlist1') inlist_star2_binary = os.path.join(path, 'binary', 'inlist2') @@ -1547,6 +1666,8 @@ def setup_grid_run_folder(path, final_columns, final_extras, final_inlists, verb final_inlists['star2_controls'], final_inlists['star2_job']) + logger.info('MESA inlist files written successfully.') + # Essentials paths created in this functions output_paths = { 'binary_executable': os.path.join(path, 'binary', 'binary'), @@ -1659,7 +1780,7 @@ def generate_submission_scripts(submission_type, command_line, slurm, nr_systems if submission_type == 'shell': script_name = 'grid_command.sh' if os.path.exists(script_name): - Pwarn(f'Replace {script_name}', "OverwriteWarning") + logger.warning(f'Replacing existing script: {script_name}') with open(script_name, 'w') as f: f.write('#!/bin/bash\n\n') @@ -1680,7 +1801,7 @@ def generate_submission_scripts(submission_type, command_line, slurm, nr_systems _write_cleanup_commands(f, slurm) os.system(f"chmod 755 {script_name}") - print(f"Created {script_name}") + logger.debug(f"Created {os.path.abspath(script_name)}") elif submission_type == 'slurm': # Generate main grid submission script From a271efb935cf33e3151f75705867dcc309f86225 Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 23 Jan 2026 22:03:22 +0100 Subject: [PATCH 03/13] move to posydon-grid --- bin/posydon-grid | 139 ++++ bin/posydon-setup-grid | 1221 +++------------------------------ posydon/CLI/grids/__init__.py | 8 + posydon/CLI/grids/setup.py | 385 +++++++++++ 4 files changed, 611 insertions(+), 1142 deletions(-) create mode 100644 bin/posydon-grid diff --git a/bin/posydon-grid b/bin/posydon-grid new file mode 100644 index 0000000000..aa65fb3e73 --- /dev/null +++ b/bin/posydon-grid @@ -0,0 +1,139 @@ +#!/usr/bin/env python +"""POSYDON Grid Management CLI. + +A unified command-line interface for managing POSYDON MESA grids. +Supports subcommands for setting up, running, and managing grid computations. + +Usage: + posydon-grid setup --inifile --grid-type [options] + posydon-grid --help + +Author: Max Briel +""" + +import argparse +import os +import sys + +from posydon.CLI.grids.setup import run_setup + + +def create_setup_parser(subparsers): + """Create the 'setup' subcommand parser. + + Parameters + ---------- + subparsers : argparse._SubParsersAction + The subparsers object from the main parser + + Returns + ------- + argparse.ArgumentParser + The setup subcommand parser + """ + setup_parser = subparsers.add_parser( + 'setup', + help='Setup a MESA grid run', + description='Configure and prepare a MESA grid for execution. ' + 'This includes reading configuration, setting up inlists, ' + 'and generating submission scripts.' + ) + + setup_parser.add_argument( + "--inifile", + help="Path to the ini file containing grid parameters", + required=True + ) + setup_parser.add_argument( + "--grid-type", + help="Type of grid: 'fixed' for a predefined grid of points, " + "'dynamic' for sampling new points from a pre-computed MESA grid", + required=True, + choices=['fixed', 'dynamic'] + ) + setup_parser.add_argument( + "--run-directory", + help="Path where executable will be made and MESA simulation output " + "will be placed (default: current directory)", + default=os.getcwd() + ) + setup_parser.add_argument( + "--submission-type", + help="Type of submission script to generate: 'shell' or 'slurm'", + default='shell', + choices=['slurm', 'shell'] + ) + setup_parser.add_argument( + "-n", "--nproc", + help="Number of processors", + type=int, + default=1 + ) + setup_parser.add_argument( + "-v", "--verbose", + nargs="?", + const="console", + default=None, + help="Enable verbose logging. Optionally specify a filename to log to " + "(e.g., -v logfile.txt). If no filename provided, logs to console." + ) + + return setup_parser + + +def parse_commandline(): + """Parse command-line arguments. + + Returns + ------- + argparse.Namespace + Parsed command-line arguments + """ + parser = argparse.ArgumentParser( + prog='posydon-grid', + description='POSYDON Grid Management CLI - A unified interface for ' + 'managing POSYDON MESA grids.', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + posydon-grid setup --inifile config.ini --grid-type fixed + posydon-grid setup --inifile config.ini --grid-type dynamic --submission-type slurm + +For more information on a specific command, use: + posydon-grid --help +''' + ) + + subparsers = parser.add_subparsers( + dest='command', + title='Available commands', + description='Use "posydon-grid --help" for more info on a command' + ) + + # Add the setup subcommand + create_setup_parser(subparsers) + + args = parser.parse_args() + + # If no command is specified, print help and exit + if args.command is None: + parser.print_help() + sys.exit(1) + + return args + + +def main(): + """Main entry point for the posydon-grid CLI.""" + args = parse_commandline() + + if args.command == 'setup': + run_setup(args) + else: + # This shouldn't happen with proper subparser setup + print(f"Unknown command: {args.command}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/bin/posydon-setup-grid b/bin/posydon-setup-grid index 31550dca9a..697476b38b 100755 --- a/bin/posydon-setup-grid +++ b/bin/posydon-setup-grid @@ -1,1160 +1,97 @@ #!/usr/bin/env python -############################################################################## -# IMPORT ALL NECESSARY PYTHON PACKAGES -############################################################################## +"""Setup a POSYDON MESA grid run. + +DEPRECATED: This command is deprecated. Please use 'posydon-grid setup' instead. + +This script is maintained for backward compatibility and will be removed +in a future version. + +Usage: + posydon-setup-grid --inifile --grid-type [options] + + Preferred alternative: + posydon-grid setup --inifile --grid-type [options] +""" import argparse -import glob -import logging import os -import shutil -import subprocess - -import pandas +import sys +import warnings -from posydon.active_learning.psy_cris.utils import parse_inifile -from posydon.CLI.grids.setup import ( - construct_command_line, - generate_submission_scripts, - get_additional_user_settings, - read_grid_file, - resolve_columns, - resolve_extras, - resolve_inlists, - setup_grid_run_folder, - setup_inlist_repository, - setup_logger, - setup_MESA_defaults, - setup_POSYDON, - setup_user, -) -from posydon.grids.psygrid import PSyGrid -from posydon.utils import configfile -from posydon.utils import gridutils as utils -from posydon.utils.posydonwarning import Pwarn +from posydon.CLI.grids.setup import run_setup -# Setup logger -logger = logging.getLogger(__name__) -############################################################################### -# DEFINE COMMANDLINE ARGUMENTS -############################################################################### def parse_commandline(): - """Parse the arguments given on the command-line. - """ - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--inifile", - help="Name of ini file of params", - required=True) - parser.add_argument("--grid-type", - help="Either you are supplying a grid " - "of points to run MESA on (fixed) or you are supplying a pre computed MESA " - "grid and want to sample new points to run MESA on (dynamic).", - required=True) - parser.add_argument("--run-directory", - help="Path where executable will be made and MESA " - "simulation output will be placed", default=os.getcwd()) - parser.add_argument("--submission-type", - help="Options include creating a shell script or a slurm script", - default='shell') - parser.add_argument("-n", "--nproc", - help="number of processors", type=int, default=1) - parser.add_argument("-v", "--verbose", nargs="?", const="console", default=None, - help="Enable verbose logging. Optionally specify a filename to log to (e.g., -v logfile.txt). If no filename provided, logs to console.") + """Parse the arguments given on the command-line.""" + parser = argparse.ArgumentParser( + description='Setup a POSYDON MESA grid run.\n\n' + 'DEPRECATED: Please use "posydon-grid setup" instead.', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--inifile", + help="Name of ini file of params", + required=True + ) + parser.add_argument( + "--grid-type", + help="Either you are supplying a grid of points to run MESA on (fixed) " + "or you are supplying a pre-computed MESA grid and want to sample " + "new points to run MESA on (dynamic).", + required=True, + choices=['fixed', 'dynamic'] + ) + parser.add_argument( + "--run-directory", + help="Path where executable will be made and MESA simulation output " + "will be placed", + default=os.getcwd() + ) + parser.add_argument( + "--submission-type", + help="Options include creating a shell script or a slurm script", + default='shell', + choices=['slurm', 'shell'] + ) + parser.add_argument( + "-n", "--nproc", + help="Number of processors", + type=int, + default=1 + ) + parser.add_argument( + "-v", "--verbose", + nargs="?", + const="console", + default=None, + help="Enable verbose logging. Optionally specify a filename to log to " + "(e.g., -v logfile.txt). If no filename provided, logs to console." + ) args = parser.parse_args() - if args.grid_type not in ['fixed', 'dynamic']: - raise parser.error("--grid-type must be either fixed or dynamic") - - if args.submission_type not in ['slurm', 'shell']: - raise parser.error('--submission-type must be either slurm of shell') + # Add command attribute for compatibility with run_setup + args.command = 'setup' return args -# def find_inlist_from_scenario(source, gitcommit, system_type): -# """Dynamically find the inlists the user wants to from the supplied info - -# Parameters -# ---------- - -# source: - -# gitcommit: - -# system_type: -# """ -# # note the directory we are in now -# where_am_i_now = os.getcwd() -# print("We are going to dynamically fetch the posydon inlists based on your scenario") -# if source == 'posydon': -# print("You have selected posydon as your source") -# print("checking if we have already cloned POSYDON-MESA-INLISTS for you") -# if not os.path.isdir('{0}/.posydon_mesa_inlists'.format(os.environ['HOME'])): -# print("We are clonining the repo for you") -# # Determine location of executables -# proc = subprocess.Popen(['git', 'clone', 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git', '{0}/.posydon_mesa_inlists'.format(os.environ['HOME'])], -# stdin = subprocess.PIPE, -# stdout = subprocess.PIPE, -# stderr = subprocess.PIPE -# ) -# (clone, err) = proc.communicate() -# else: -# Pwarn("git repository is already there, using that", -# "OverwriteWarning") - -# inlists_dir = '{0}/.posydon_mesa_inlists'.format(os.environ['HOME']) -# branch = gitcommit.split('-')[0] -# githash = gitcommit.split('-')[1] - -# elif source == 'user': -# print("You have selected user as your source " -# "checking if we have already cloned USER-MESA-INLISTS for you " -# "Validating the name of the git hash you want to use..." -# "must be of format 'branch-githash'") - -# if len(gitcommit.split('-')) != 2: -# raise ValueError("You have supplied an invalid user gitcommit format, must be of format 'branch-githash'") - -# branch = gitcommit.split('-')[0] -# githash = gitcommit.split('-')[1] - -# if not os.path.isdir('{0}/.user_mesa_inlists'.format(os.environ['HOME'])): -# print("We are clonining the repo for you") -# # Determine location of executables -# proc = subprocess.Popen(['git', 'clone', 'https://github.com/POSYDON-code/USER-MESA-INLISTS.git', '{0}/.user_mesa_inlists'.format(os.environ['HOME'])], -# stdin = subprocess.PIPE, -# stdout = subprocess.PIPE, -# stderr = subprocess.PIPE -# ) -# (clone, err) = proc.communicate() -# else: -# Pwarn("git repository is already there, using that", -# "OverwriteWarning") - -# inlists_dir = '{0}/.user_mesa_inlists'.format(os.environ['HOME']) -# branch = gitcommit.split('-')[0] -# githash = gitcommit.split('-')[1] - -# else: -# raise ValueError("supplied source is not valid/understood. Valid sources are user and posydon") - -# os.chdir(inlists_dir) -# print("checking out branch: {0}".format(branch)) - -# proc = subprocess.Popen(['git', 'checkout', '{0}'.format(branch)], -# stdin = subprocess.PIPE, -# stdout = subprocess.PIPE, -# stderr = subprocess.PIPE -# ) -# proc.wait() - -# print("For posterity we are pulling (specifically needed if you already have the repo clone)") -# proc = subprocess.call(['git', 'pull'], -# stdin = subprocess.PIPE, -# stdout = subprocess.PIPE, -# stderr = subprocess.PIPE -# ) - - -# print("checking out commit/tag: {0}".format(githash)) - -# proc = subprocess.Popen(['git', 'checkout', '{0}'.format(githash)], -# stdin = subprocess.PIPE, -# stdout = subprocess.PIPE, -# stderr = subprocess.PIPE -# ) -# proc.wait() - -# # if this is looking at posydon defaults, all posydon defaults build from default common inlists -# if source == 'posydon': -# inlists_location_common = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', "default_common_inlists") -# print("Based on system_type {0} " -# "We are populating the posydon inlists in the following directory: " -# "{1}".format(system_type, inlists_location_common)) - -# inlist1 = os.path.join(inlists_location_common, 'binary', 'inlist1') - -# # Max comment: This code also allows you to set the zams_filenames in the loaded inlists. -# # This is not useful to define it in multiple places. It's better to define it only once in the configuration file. -# if os.path.isfile(inlist1): -# # with open(inlist1) as f: -# # # check if we also need to find the location of the zams.data file -# # for line in f.readlines(): -# # if 'zams_filename' in line: -# # print("ZAMS_FILENAME detected, setting mesa_inlists['zams_filename'] for star 1") -# # zams_filename_1 = os.path.split(line.split("'")[1])[1] -# # zams_file_path = os.path.join(inlists_dir, 'r11701', "ZAMS_models", zams_filename_1) -# # if os.path.isfile(zams_file_path): -# # print("Verified locations of ZAMS data file, {0}".format(zams_file_path)) -# # mesa_inlists['zams_filename_1'] = "{0}".format(zams_file_path) -# print("Running Single Grid: Setting mesa_star1_extras to {0}/binary/src/run_star_extras.f".format(inlists_location_common)) - -# inlist2 = os.path.join(inlists_location_common, 'binary', 'inlist2') -# if os.path.isfile(inlist2): -# # with open(inlist2) as f: -# # # check if we also need to find the location of the zams.data file -# # for line in f.readlines(): -# # if 'zams_filename' in line: -# # print("ZAMS_FILENAME detected, setting mesa_inlists['zams_filename'] for star 2") -# # zams_filename_2 = os.path.split(line.split("'")[1])[1] -# # zams_file_path = os.path.join(inlists_dir, 'r11701', "ZAMS_models", zams_filename_2) -# # if os.path.isfile(zams_file_path): -# # print("Verified locations of ZAMS data file, {0}".format(zams_file_path)) -# # mesa_inlists['zams_filename_2'] = "{0}".format(zams_file_path) -# print("Running Single Grid: Setting mesa_star2_extras to {0}/binary/src/run_star_extras.f".format(inlists_location_common)) - - -# print("Updating inifile values") - # binary inlists - # mesa_inlists['star1_controls_posydon_defaults'] = '{0}/binary/inlist1'.format(inlists_location_common) - # mesa_inlists['star1_job_posydon_defaults'] = '{0}/binary/inlist1'.format(inlists_location_common) - # mesa_inlists['star2_controls_posydon_defaults'] = '{0}/binary/inlist2'.format(inlists_location_common) - # mesa_inlists['star2_job_posydon_defaults'] = '{0}/binary/inlist2'.format(inlists_location_common) - # mesa_inlists['binary_controls_posydon_defaults'] = '{0}/binary/inlist_project'.format(inlists_location_common) - # mesa_inlists['binary_job_posydon_defaults'] = '{0}/binary/inlist_project'.format(inlists_location_common) - - # # columns - # mesa_inlists['star_history_columns'] = '{0}/history_columns.list'.format(inlists_location_common) - # mesa_inlists['binary_history_columns'] = '{0}/binary_history_columns.list'.format(inlists_location_common) - # mesa_inlists['profile_columns'] = '{0}/profile_columns.list'.format(inlists_location_common) - - # # executables - # mesa_extras['posydon_binary_extras'] = '{0}/binary/src/run_binary_extras.f'.format(inlists_location_common) - # mesa_extras['posydon_star_binary_extras'] = '{0}/binary/src/run_star_extras.f'.format(inlists_location_common) - - # mesa_extras["mesa_star1_extras"] = '{0}/binary/src/run_star_extras.f'.format(inlists_location_common) - - # so this is sufficient for system type HMS-HMS but not for others, for others we stack the above inlists on further inlists - # we also need to see if we are looking at a folder for binaries or singles - # if system_type == 'HeMS-HMS': - # inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) - # print("Based on system_type {0} " - # "We are populating the user inlists in the following directory: " - # "{1}".format(system_type, inlists_location)) - - # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist1")): - # mesa_inlists['star1_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - # mesa_inlists['star1_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - - # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist2")): - # mesa_inlists['star2_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - # mesa_inlists['star2_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - - # # check for star formation parameters - # if os.path.isdir(os.path.join(inlists_location, "star1_formation")): - # # We are making star1 so we can unset the zams file we were going to use for star1 - # print("We are making star1 so we can unset the zams file we were going to use for star1") - # # print() - # # print("HERE") - # # print() - # mesa_inlists['zams_filename_1'] = None - - # # Figure out how many user star1 formation steps there are and layer the posydon default inlists on all of them - # star1_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) - # print("These are the user we are using to make star1: {0}".format(star1_formation_scenario)) - # print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) - # mesa_inlists['star1_formation_controls_posydon_defaults'] = [] - # mesa_inlists['star1_formation_job_posydon_defaults'] = [] - # for i in range(len(star1_formation_scenario)): - # mesa_inlists['star1_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - # mesa_inlists['star1_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - - # mesa_inlists['star1_formation_controls_user'] = star1_formation_scenario - # mesa_inlists['star1_formation_job_user'] = star1_formation_scenario - - # elif system_type != "HMS-HMS" and not mesa_inlists['single_star_grid']: - # inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) - # print("Based on system_type {0} " - # "We are populating the user inlists in the following directory: " - # "{1}".format(system_type, inlists_location)) - - # # determine where the binary inlist(s) are - # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist_project")): - # mesa_inlists['binary_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist_project")) - # mesa_inlists['binary_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist_project")) - - # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist1")): - # mesa_inlists['star1_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - # mesa_inlists['star1_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist1")) - - # if os.path.isfile(os.path.join(inlists_location, "binary", "inlist2")): - # mesa_inlists['star2_controls_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - # mesa_inlists['star2_job_user'] = '{0}'.format(os.path.join(inlists_location, "binary", "inlist2")) - - # if os.path.isfile(os.path.join(inlists_location, "history_columns.list")): - # mesa_inlists['star_history_columns'] = os.path.join(inlists_location, "history_columns.list") - - # if os.path.isfile(os.path.join(inlists_location, "binary_history_columns.list")): - # mesa_inlists['binary_history_columns'] = os.path.join(inlists_location, "binary_history_columns.list") - - # if os.path.isfile(os.path.join(inlists_location, "profile_columns.list")): - # mesa_inlists['profile_columns'] = os.path.join(inlists_location, "profile_columns.list") - - # if os.path.isfile(os.path.join(inlists_location, "src", "run_binary_extras.f")): - # mesa_extras['user_binary_extras'] = '{0}'.format(os.path.join(inlists_location, "src", "run_binary_extras.f")) - - # if os.path.isfile(os.path.join(inlists_location, "src", "run_star_extras.f")): - # mesa_extras['user_star_binary_extras'] = '{0}'.format(os.path.join(inlists_location, "src", "run_star_extras.f")) - - # # check for star formation parameters - # if os.path.isdir(os.path.join(inlists_location, "star1_formation")): - # # We are making star1 so we can unset the zams file we were going to use for star1 - # print("We are making star1 so we can unset the zams file we were going to use for star1") - # mesa_inlists['zams_filename_1'] = None - - # # Figure out how many user star1 formation steps there are and layer the posydon default inlists on all of them - # star1_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) - # print("These are the user we are using to make star1: {0}".format(star1_formation_scenario)) - # print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) - # mesa_inlists['star1_formation_controls_posydon_defaults'] = [] - # mesa_inlists['star1_formation_job_posydon_defaults'] = [] - # for i in range(len(star1_formation_scenario)): - # mesa_inlists['star1_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - # mesa_inlists['star1_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - - # mesa_inlists['star1_formation_controls_user'] = star1_formation_scenario - # mesa_inlists['star1_formation_job_user'] = star1_formation_scenario - - # if os.path.isdir(os.path.join(inlists_location, "star2_formation")): - # # We are making star2 so we can unset the zams file we were going to use for star2 - # print("We are making star2 so we can unset the zams file we were going to use for star2") - # mesa_inlists['zams_filename_2'] = None - - # # Figure out how many user star2 formation steps there are and layer the posydon default inlists on all of them - # star2_formation_scenario = sorted(glob.glob(os.path.join(inlists_location, "star2_formation", "*step*"))) - # print("These are the user we are using to make star2: {0}".format(star2_formation_scenario)) - # print("We are going to add a layer of posydon default common inlists to these user steps: {0}".format('{0}/binary/inlist1'.format(inlists_location_common))) - # mesa_inlists['star2_formation_controls_posydon_defaults'] = [] - # mesa_inlists['star2_formation_job_posydon_defaults'] = [] - # for i in range(len(star2_formation_scenario)): - # mesa_inlists['star2_formation_controls_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - # mesa_inlists['star2_formation_job_posydon_defaults'].append('{0}/binary/inlist1'.format(inlists_location_common)) - - # mesa_inlists['star2_formation_controls_user'] = star2_formation_scenario - # mesa_inlists['star2_formation_job_user'] = star2_formation_scenario - - # if system_type == "HMS-HMS" and mesa_inlists['single_star_grid']: - # print("You want a single star HMS grid, this means that we need to make a user inlist on the fly with a single line " - # "x_logical_ctrl(1)=.true.") - # # write star1 formation step to file - # special_single_star_user_inlist = os.path.join(os.getcwd(), "special_single_star_user_inlist") - # if os.path.exists(special_single_star_user_inlist): - # Pwarn('Replace '+special_single_star_user_inlist, - # "OverwriteWarning") - # with open(special_single_star_user_inlist, 'wb') as f: - # f.write(b'&controls\n\n') - # f.write('\t{0} = {1}\n'.format("x_logical_ctrl(1)", ".true.").encode('utf-8')) - - # f.write(b'\n\n') - - # f.write(b""" - # / ! end of star1_controls namelist - - # """) - # mesa_inlists['star1_controls_special'] = special_single_star_user_inlist - # elif system_type == "CO-He_star" and mesa_inlists['single_star_grid']: - # inlists_location = '{0}/{1}/{2}/'.format(inlists_dir, 'r11701', system_type) - # print("Based on system_type {0} " - # "We are populating the user inlists in the following directory: " - # "{1}".format(system_type, inlists_location)) - - # # We are making star2 so we can unset the zams file we were going to use for star2 - # print("We are making star2 so we can unset the zams file we were going to use for star2") - # mesa_inlists['zams_filename_2'] = None - - # # Find the user single star controls - # single_star_scenario = sorted(glob.glob(os.path.join(inlists_location, "star1_formation", "*step*"))) - # print("These are the user inlists used in the single star grid: {0}".format(single_star_scenario)) - # mesa_inlists['star1_controls_user'] = single_star_scenario - # mesa_inlists['star1_job_user'] = single_star_scenario - # print("You want a single star He grid, " - # "this means that we need to make the inlist that will be used to evolve the system " - # "and make sure we layer on the line " - # "x_logical_ctrl(1)=.true.") - # # write star1 formation step to file - # special_single_star_user_inlist = os.path.join(os.getcwd(), "special_single_star_user_inlist") - # if os.path.exists(special_single_star_user_inlist): - # Pwarn('Replace '+special_single_star_user_inlist, - # "OverwriteWarning") - # with open(special_single_star_user_inlist, 'wb') as f: - # f.write(b'&controls\n\n') - # f.write('\t{0} = {1}\n'.format("x_logical_ctrl(1)", ".true.").encode('utf-8')) - - # f.write(b'\n\n') - # f.write(b""" - # / ! end of star1_controls namelist - - # """) - # f.write(b'&star_job\n\n') - # f.write(b""" - # / / ! end of star_job namelist - - # """) - # mesa_inlists['star1_controls_special'].append(special_single_star_user_inlist) - - # # change back to where I was - # os.chdir(where_am_i_now) - - # return - -# def construct_static_inlist(mesa_inlists, grid_parameters, working_directory=os.getcwd()): -# """Based on all the inlists that were passed construc the MESA project dir - -# Parameters -# mesa_inlists: -# All of the values from the mesa_inlists section of the inifile (`dict`) - -# grid_parameters: -# A list of the parameters from the csv file so we can determine all of -# the inlist parameters for binary, star1 and star2 that will be changing -# with this grid - -# Returns: -# inlists -# """ - # To make it backwards compatible - # if 'zams_filename' in mesa_inlists.keys(): - # mesa_inlists['zams_filename_1'] = mesa_inlists['zams_filename'] - # mesa_inlists['zams_filename_2'] = mesa_inlists['zams_filename'] - - # if 'zams_filename_1' not in mesa_inlists.keys(): - # mesa_inlists['zams_filename_1'] = None - - # if 'zams_filename_2' not in mesa_inlists.keys(): - # mesa_inlists['zams_filename_2'] = None - - # if 'single_star_grid' not in mesa_inlists.keys(): - # mesa_inlists['single_star_grid'] = False - - ######################################## - ### CONSTRUCT BINARY INLIST PARAMS ### - ######################################## - - # # inlist project controls binary_job and binary_controls - # inlist_binary_project = os.path.join(working_directory, 'binary', 'inlist_project') - # # inlist1 (controls star_job1 and star_controls1 - # inlist_star1_binary = os.path.join(working_directory, 'binary', 'inlist1') - # # inlist1 (controls star_job2 and star_controls2 - # inlist_star2_binary = os.path.join(working_directory, 'binary', 'inlist2') - - # # Initialize some stuff - # final_binary_controls = {} - # final_binary_job = {} - # final_star1_binary_job = {} - # final_star2_binary_job = {} - # final_star1_binary_controls = {} - # final_star2_binary_controls = {} - - # if not mesa_inlists['single_star_grid']: - # for k, v in mesa_inlists.items(): - # if v is not None: - # if 'binary_controls' in k: - # section = '&binary_controls' - # controls_dict = utils.clean_inlist_file(v, section=section) - # for k1,v1 in controls_dict[section].items(): - # # remove any hidden inlists extras since that is not how we want to do things - # if ('read_extra' in k1) or ('inlist' in k1): continue - # final_binary_controls[k1] = v1 - - # elif 'binary_job' in k: - # section = '&binary_job' - # job_dict = utils.clean_inlist_file(v, section=section) - # for k1,v1 in job_dict[section].items(): - # # remove any hidden inlists extras since that is not how we want to do things - # if ('read_extra' in k1) or ('inlist' in k1): continue - # final_binary_job[k1] = v1 - - # elif 'star1_job' in k: - # section = '&star_job' - # star_job1_dict = utils.clean_inlist_file(v, section=section) - # for k1,v1 in star_job1_dict[section].items(): - # # remove any hidden inlists extras since that is not how we want to do things - # if ('read_extra' in k1) or ('inlist' in k1): continue - # final_star1_binary_job[k1] = v1 - - # elif 'star2_job' in k: - # section = '&star_job' - # star_job2_dict = utils.clean_inlist_file(v, section=section) - # for k1,v1 in star_job2_dict[section].items(): - # # remove any hidden inlists extras since that is not how we want to do things - # if ('read_extra' in k1) or ('inlist' in k1): continue - # final_star2_binary_job[k1] = v1 - - # elif 'star1_controls' in k: - # section = '&controls' - # star_control1_dict = utils.clean_inlist_file(v, section=section) - # for k1,v1 in star_control1_dict[section].items(): - # # remove any hidden inlists extras since that is not how we want to do things - # if ('read_extra' in k1) or ('inlist' in k1): continue - # if 'num_x_ctrls' in k1: - # # This is a special default that the default value in the .defaults - # # file in MESA does not work because it is a placeholder - # final_star1_binary_controls[k1.replace('num_x_ctrls','1')] = v1 - # else: - # final_star1_binary_controls[k1] = v1 - - # elif 'star2_controls' in k: - # section = '&controls' - # star_control2_dict = utils.clean_inlist_file(v, section=section) - # for k1,v1 in star_control2_dict[section].items(): - # # remove any hidden inlists extras since that is not how we want to do things - # if ('read_extra' in k1) or ('inlist' in k1): continue - # if 'num_x_ctrls' in k1: - # # This is a special default that the default value in the .defaults - # # file in MESA does not work because it is a placeholder - # final_star2_binary_controls[k1.replace('num_x_ctrls','1')] = v1 - # else: - # final_star2_binary_controls[k1] = v1 - - # detemine which is any of the parameters are binary_controls or binary_job params - # grid_params_binary_controls = [param for param in grid_parameters if param in final_binary_controls.keys()] - # print("Grid parameters that effect binary_controls: {0}".format(','.join(grid_params_binary_controls))) - # grid_params_binary_job = [param for param in grid_parameters if param in final_binary_job.keys()] - # print("Grid parameters that effect binary_job: {0}".format(','.join(grid_params_binary_job))) - - # grid_params_star1_binary_controls = [param for param in grid_parameters if param in final_star1_binary_controls.keys()] - # print("Grid parameters that effect star1_binary_controls: {0}".format(','.join(grid_params_star1_binary_controls))) - # grid_params_star1_binary_job = [param for param in grid_parameters if param in final_star1_binary_job.keys()] - # print("Grid parameters that effect star1_binary_job: {0}".format(','.join(grid_params_star1_binary_job))) - - # grid_params_star2_binary_controls = [param for param in grid_parameters if param in final_star2_binary_controls.keys()] - # print("Grid parameters that effect star2_binary_controls: {0}".format(','.join(grid_params_star2_binary_controls))) - # grid_params_star2_binary_job = [param for param in grid_parameters if param in final_star2_binary_job.keys()] - # print("Grid parameters that effect star2_binary_job: {0}".format(','.join(grid_params_star2_binary_job))) - - # depending on if there are any grid parameters that effect star1 or star2 we need to actually - # do a read star extras step - # if grid_params_star1_binary_controls: - # final_star1_binary_controls['read_extra_controls_inlist1'] = '.true.' - # final_star1_binary_controls['extra_controls_inlist1_name'] = "'inlist_grid_star1_binary_controls'" - - # if grid_params_star2_binary_controls: - # final_star2_binary_controls['read_extra_controls_inlist1'] = '.true.' - # final_star2_binary_controls['extra_controls_inlist1_name'] = "'inlist_grid_star2_binary_controls'" - - # if grid_params_star1_binary_job: - # final_star1_binary_job['read_extra_star_job_inlist1'] = '.true.' - # final_star1_binary_job['extra_star_job_inlist1_name'] = "'inlist_grid_star1_binary_job'" - - # if grid_params_star2_binary_job: - # final_star2_binary_job['read_extra_star_job_inlist1'] = '.true.' - # final_star2_binary_job['extra_star_job_inlist1_name'] = "'inlist_grid_star2_binary_job'" - - # We want to point the binary_job section to the star1 and star2 inlist we just made - #final_binary_job['inlist_names(1)'] = "'{0}'".format(inlist_star1_binary) - #final_binary_job['inlist_names(2)'] = "'{0}'".format(inlist_star2_binary) - - ######################## - ### STAR 1 FORMATION ### - ######################## - # Check the number of inlists provided to the star1 formation sections - # of the inifile -# star1_formation = {} - -# # if we have provided a pre-computed zams model, it does not matter if we wanted to form star1 and star2 for -# # the binary step, we have supceded this with the zams_filename -# if (mesa_inlists['zams_filename_1'] is not None) and (not mesa_inlists['single_star_grid']): -# star1_formation_dictionary = {} -# elif mesa_inlists['single_star_grid']: -# star1_formation_dictionary = dict(filter(lambda elem: (('star1_job' in elem[0]) or ('star1_controls' in elem[0])) and elem[1] is not None, mesa_inlists.items())) -# else: -# # create dictionary of only these sections -# star1_formation_dictionary = dict(filter(lambda elem: 'star1_formation' in elem[0] and elem[1] is not None, mesa_inlists.items())) -# # See if the user even supplied inlists for doing star1_formation -# if star1_formation_dictionary: -# # initialize the string argument for star1 formation that will be passed to posydon-run-grid -# inlist_star1_formation = '' -# # check the number of inlists in each star1 formation parameter. We will take calculate the max number and treat that as -# # the number of star1 formation steps desired before making the final star1 model that will be fed into the binary exectuable -# number_of_star1_formation_steps = 1 -# for k, v in star1_formation_dictionary.items(): -# if type(v) == list: -# number_of_star1_formation_steps = max(number_of_star1_formation_steps, len(v)) +def main(): + """Main entry point for posydon-setup-grid (deprecated).""" + warnings.warn( + "posydon-setup-grid is deprecated and will be removed in a future version. " + "Please use 'posydon-grid setup' instead.", + DeprecationWarning, + stacklevel=2 + ) + print( + "\033[93mWarning: posydon-setup-grid is deprecated. " + "Please use 'posydon-grid setup' instead.\033[0m", + file=sys.stderr + ) -# for step in range(number_of_star1_formation_steps): -# star1_formation['step{0}'.format(step)] = {} -# star1_formation['step{0}'.format(step)]['inlist_file'] = os.path.join(working_directory, 'star1', 'inlist_step{0}'.format(step)) -# for k, v in star1_formation_dictionary.items(): -# star1_formation['step{0}'.format(step)][k] = v[step] if type(v) == list else v - -# # Now we loop over each star1 formation step and construct the final star1 formation inlist for each step -# for step, step_inlists in enumerate(star1_formation.values()): -# # there is a new one of these final star1 formation inlists per step -# final_star1_formation_controls = {} -# final_star1_formation_job = {} -# for k, v in step_inlists.items(): -# if ('star1_formation_controls' in k) or ('star1_controls' in k): -# section = '&controls' -# controls_dict = utils.clean_inlist_file(v, section=section) -# for k1,v1 in controls_dict[section].items(): -# # remove any hidden inlists extras since that is not how we want to do things -# if ('read_extra' in k1) or ('inlist' in k1): continue -# if 'num_x_ctrls' in k1: -# # This is a special default that the default value in the .defaults -# # file in MESA does not work because it is a placeholder -# final_star1_formation_controls[k1.replace('num_x_ctrls','1')] = v1 -# else: -# final_star1_formation_controls[k1] = v1 - -# if ('star1_formation_job' in k) or ('star1_job' in k): -# section = '&star_job' -# controls_dict = utils.clean_inlist_file(v, section=section) -# for k1,v1 in controls_dict[section].items(): -# # remove any hidden inlists extras since that is not how we want to do things -# if ('read_extra' in k1) or ('inlist' in k1): continue -# final_star1_formation_job[k1] = v1 - -# # The user supplied a way to form star1 and we need to update dictionary of parameters and their values correctly -# # We want to make sure that the binary inlists load up the properly saved models from star1 formation -# final_star1_binary_job['create_pre_main_sequence_model'] = ".false." -# final_star1_binary_job['load_saved_model'] = ".true." -# final_star1_binary_job['saved_model_name'] = "'initial_star1_step{0}.mod'".format(step) -# # if this is step0 then we simply overwrite the save_model_when_terminate and -# # save_model_filename parts of the inlists. However, for all steps higher than -# # step 0 we need to have that step load in the model from the step -# # below the current step -# if step == 0: -# final_star1_formation_job['save_model_when_terminate'] = '.true.' -# final_star1_formation_job['save_model_filename'] = "'initial_star1_step{0}.mod'".format(step) -# else: -# final_star1_formation_job['create_pre_main_sequence_model'] = ".false." -# final_star1_formation_job['load_saved_model'] = ".true." -# final_star1_formation_job['saved_model_name'] = "'initial_star1_step{0}.mod'".format(step-1) -# final_star1_formation_job['save_model_when_terminate'] = '.true.' -# final_star1_formation_job['save_model_filename'] = "'initial_star1_step{0}.mod'".format(step) - -# if (mesa_inlists['zams_filename_1'] is not None) and (mesa_inlists['single_star_grid']): -# final_star1_formation_controls['zams_filename'] = "'{0}'".format(mesa_inlists['zams_filename_1']) -# elif (mesa_inlists['zams_filename_1'] is None) and (mesa_inlists['single_star_grid']) and ('zams_filename_1' in final_star1_formation_controls.keys()): -# final_star1_formation_controls.pop("zams_filename", None) - -# # write star1 formation step to file -# if os.path.exists(step_inlists['inlist_file']): -# Pwarn('Replace '+step_inlists['inlist_file'], -# "OverwriteWarning") -# with open(step_inlists['inlist_file'], 'wb') as f: -# f.write(b'&controls\n\n') -# for k,v in final_star1_formation_controls.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b'\n\n') - -# f.write(b""" -# / ! end of star1_controls namelist - -# """) - -# f.write(b'&star_job\n\n') -# for k,v in final_star1_formation_job.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b""" -# / ! end of star1_job namelist - -# """) -# # Construct star1 formation argument string to be passed to posydon-run-grid -# inlist_star1_formation += ' {0}'.format(step_inlists['inlist_file']) -# else: -# inlist_star1_formation = None - -# ######################## -# ### STAR 2 FORMATION ### -# ######################## -# # Check the number of inlists provided to the star2 formation sections -# # of the inifile -# star2_formation = {} - -# # if we have provided a pre-computed zams model, it does not matter if we wanted to form star1 and star2 for -# # the binary step, we have supceded this with the zams_filename_2 -# if mesa_inlists['zams_filename_2'] is not None: -# star2_formation_dictionary = {} -# else: -# # create dictionary of only these sections -# star2_formation_dictionary = dict(filter(lambda elem: 'star2_formation' in elem[0] and elem[1] is not None, mesa_inlists.items())) - -# # See if the user even supplied inlists for doing star2_formation -# if star2_formation_dictionary: -# # initialize the string argument for star2 formation that will be passed to posydon-run-grid -# inlist_star2_formation = '' -# # check the number of inlists in each star2 formation parameter. We will take calculate the max number and treat that as -# # the number of star2 formation steps desired before making the final star2 model that will be fed into the binary exectuable -# number_of_star2_formation_steps = 1 -# for k, v in star2_formation_dictionary.items(): -# if type(v) == list: -# number_of_star2_formation_steps = max(number_of_star2_formation_steps, len(v)) - -# for step in range(number_of_star2_formation_steps): -# star2_formation['step{0}'.format(step)] = {} -# star2_formation['step{0}'.format(step)]['inlist_file'] = os.path.join(working_directory, 'star2', 'inlist_step{0}'.format(step)) -# for k, v in star2_formation_dictionary.items(): -# star2_formation['step{0}'.format(step)][k] = v[step] if type(v) == list else v - -# # Now we loop over each star2 formation step and construct the final star2 formation inlist for each step -# for step, step_inlists in enumerate(star2_formation.values()): -# final_star2_formation_controls = {} -# final_star2_formation_job = {} -# for k, v in step_inlists.items(): -# if 'star2_formation_controls' in k: -# section = '&controls' -# controls_dict = utils.clean_inlist_file(v, section=section) -# for k1,v1 in controls_dict[section].items(): -# # remove any hidden inlists extras since that is not how we want to do things -# if ('read_extra' in k1) or ('inlist' in k1): continue -# if 'num_x_ctrls' in k1: -# # This is a special default that the default value in the .defaults -# # file in MESA does not work because it is a placeholder -# final_star2_formation_controls[k1.replace('num_x_ctrls','1')] = v1 -# else: -# final_star2_formation_controls[k1] = v1 - -# if 'star2_formation_job' in k: -# section = '&star_job' -# controls_dict = utils.clean_inlist_file(v, section=section) -# for k1,v1 in controls_dict[section].items(): -# # remove any hidden inlists extras since that is not how we want to do things -# if ('read_extra' in k1) or ('inlist' in k1): continue -# final_star2_formation_job[k1] = v1 - -# # then the user supplied a star2 formation and we need to update dictionary of parameters and their values correctly -# # We want to make sure that the binary inlists load up the properly saved models from star2 formation -# final_star2_binary_job['create_pre_main_sequence_model'] = ".false." -# final_star2_binary_job['load_saved_model'] = ".true." -# final_star2_binary_job['saved_model_name'] = "'initial_star2_step{0}.mod'".format(step) -# # if this is step0 then we simply overwrite the save_model_when_terminate and -# # save_model_filename parts of the inlists. However, for all steps higher than -# # step 0 we need to have that step load in the model from the step -# # below the current step -# if step == 0: -# final_star2_formation_job['save_model_when_terminate'] = '.true.' -# final_star2_formation_job['save_model_filename'] = "'initial_star2_step{0}.mod'".format(step) -# else: -# final_star2_formation_job['create_pre_main_sequence_model'] = ".false." -# final_star2_formation_job['load_saved_model'] = ".true." -# final_star2_formation_job['saved_model_name'] = "'initial_star2_step{0}.mod'".format(step-1) -# final_star2_formation_job['save_model_when_terminate'] = '.true.' -# final_star2_formation_job['save_model_filename'] = "'initial_star2_step{0}.mod'".format(step) - -# if os.path.exists(step_inlists['inlist_file']): -# Pwarn('Replace '+step_inlists['inlist_file'], -# "OverwriteWarning") -# with open(step_inlists['inlist_file'], 'wb') as f: -# f.write(b'&controls\n\n') -# for k,v in final_star2_formation_controls.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b'\n\n') - -# f.write(b""" -# / ! end of star2_controls namelist - -# """) - -# f.write(b'&star_job\n\n') -# for k,v in final_star2_formation_job.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b""" -# / ! end of star2_job namelist - -# """) -# # Construct star2 formation argument string to be passed to posydon-run-grid -# inlist_star2_formation += ' {0}'.format(step_inlists['inlist_file']) -# else: -# inlist_star2_formation = None - - - -# ########################################## -# ###### WRITE MESA BINARY INLISTS ####### -# ########################################## -# # now that we have all the parameters and their correct values -# # we now write our own inlist_project, inlist1 and inlist2 for the binary -# if os.path.exists(inlist_binary_project): -# Pwarn('Replace '+inlist_binary_project, "OverwriteWarning") -# with open(inlist_binary_project, 'wb') as f: -# f.write(b'&binary_controls\n\n') -# for k,v in final_binary_controls.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b'\n/ ! end of binary_controls namelist') - -# f.write(b'\n\n') - -# f.write(b'&binary_job\n\n') -# for k,v in final_binary_job.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b""" -# / ! end of binary_job namelist -# """) - -# if os.path.exists(inlist_star1_binary): -# Pwarn('Replace '+inlist_star1_binary, "OverwriteWarning") -# with open(inlist_star1_binary, 'wb') as f: -# f.write(b'&controls\n\n') -# for k,v in final_star1_binary_controls.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b'\n\n') - -# f.write(b""" -# / ! end of star1_controls namelist - -# """) - -# f.write(b'&star_job\n\n') -# for k,v in final_star1_binary_job.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b""" -# / ! end of star1_job namelist - -# """) - -# if os.path.exists(inlist_star2_binary): -# Pwarn('Replace '+inlist_star2_binary, "OverwriteWarning") -# with open(inlist_star2_binary, 'wb') as f: -# f.write(b'&controls\n\n') -# for k,v in final_star2_binary_controls.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b'\n\n') - -# f.write(b""" -# / ! end of star2_controls namelist - -# """) - -# f.write(b'&star_job\n\n') -# for k,v in final_star2_binary_job.items(): -# f.write('\t{0} = {1}\n'.format(k,v).encode('utf-8')) - -# f.write(b""" -# / ! end of star2_job namelist - -# """) - -# return inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, inlist_star2_binary - -# def make_executables(mesa_extras, working_directory=os.getcwd()): -# """Pass mesa extra function and compile binary executable on the fly -# """ - -# # First, make individual star executables -# star1_src_folder = os.path.join(working_directory, 'star1', 'src') -# if os.path.exists(star1_src_folder): shutil.rmtree(star1_src_folder) -# os.makedirs(star1_src_folder) - -# star1_make_folder = os.path.join(working_directory, 'star1', 'make') -# if os.path.exists(star1_make_folder): shutil.rmtree(star1_make_folder) -# os.makedirs(star1_make_folder) - -# star2_src_folder = os.path.join(working_directory, 'star2', 'src') -# if os.path.exists(star2_src_folder): shutil.rmtree(star2_src_folder) -# os.makedirs(star2_src_folder) - -# star2_make_folder = os.path.join(working_directory, 'star2', 'make') -# if os.path.exists(star2_make_folder): shutil.rmtree(star2_make_folder) -# os.makedirs(star2_make_folder) - -# # Now make the binary folder -# binary_src_folder = os.path.join(working_directory, 'binary', 'src') -# if os.path.exists(binary_src_folder): shutil.rmtree(binary_src_folder) -# os.makedirs(binary_src_folder) - -# binary_make_folder = os.path.join(working_directory, 'binary', 'make') -# if os.path.exists(binary_make_folder): shutil.rmtree(binary_make_folder) -# os.makedirs(binary_make_folder) - -# if os.path.exists('mk'): -# Pwarn('Replace mk', "OverwriteWarning") -# with open('mk', "w") as f: -# # first we need to cd into the make folder -# for k, v in mesa_extras.items(): -# if v is not None: -# if ('binary_extras' in k) or ('binary_run' in k): -# shutil.copy(v, binary_src_folder) -# elif ('star_run' in k): -# shutil.copy(v, star1_src_folder) -# shutil.copy(v, star2_src_folder) -# elif ('star1_extras' in k): -# shutil.copy(v, star1_src_folder) -# elif ('star2_extras' in k): -# shutil.copy(v, star2_src_folder) -# elif 'makefile_binary' in k: -# shutil.copy(v, os.path.join(binary_make_folder, k)) -# f.write('cd {0}\n'.format(binary_make_folder)) -# f.write('make -f {0}\n'.format(k)) -# elif 'makefile_star' in k: -# shutil.copy(v, os.path.join(star1_make_folder, k)) -# f.write('cd {0}\n'.format(star1_make_folder)) -# f.write('make -f {0}\n'.format(k)) -# shutil.copy(v, os.path.join(star2_make_folder, k)) -# f.write('cd {0}\n'.format(star2_make_folder)) -# f.write('make -f {0}\n'.format(k)) -# elif 'mesa_dir' == k: -# continue -# else: -# shutil.copy(v, working_directory) - -# os.system("chmod 755 mk") -# os.system('./mk') -# return os.path.join(working_directory,'binary','binary'), \ -# os.path.join(working_directory,'star1','star'), \ -# os.path.join(working_directory,'star2','star'), \ - -# def construct_command_line(number_of_mpi_processes, path_to_grid, -# binary_exe, star1_exe, star2_exe, -# inlist_binary_project, inlist_star1_binary, inlist_star2_binary, -# inlist_star1_formation, inlist_star2_formation, -# star_history_columns, binary_history_columns, profile_columns, -# run_directory, grid_type, path_to_run_grid_exec, -# psycris_inifile=None, keep_profiles=False, -# keep_photos=False): -# """Based on the inifile construct the command line call to posydon-run-grid -# """ -# if grid_type == "fixed": -# command_line = 'python {15} --mesa-grid {1} --mesa-binary-executable {2} ' -# elif grid_type == "dynamic": -# command_line = 'mpirun --bind-to none -np {0} python -m mpi4py {15} --mesa-grid {1} --mesa-binary-executable {2} ' -# else: -# raise ValueError("grid_type can either be fixed or dynamic not anything else") -# command_line += '--mesa-star1-executable {3} --mesa-star2-executable {4} --mesa-binary-inlist-project {5} ' -# command_line += '--mesa-binary-inlist1 {6} --mesa-binary-inlist2 {7} --mesa-star1-inlist-project {8} ' -# command_line += '--mesa-star2-inlist-project {9} --mesa-star-history-columns {10} ' -# command_line += '--mesa-binary-history-columns {11} --mesa-profile-columns {12} ' -# command_line += '--output-directory {13} --grid-type {14} ' -# command_line += '--psycris-inifile {16}' -# if keep_profiles: -# command_line += ' --keep_profiles' -# if keep_photos: -# command_line += ' --keep_photos' -# command_line = command_line.format(number_of_mpi_processes, -# path_to_grid, -# binary_exe, -# star1_exe, -# star2_exe, -# inlist_binary_project, -# inlist_star1_binary, -# inlist_star2_binary, -# inlist_star1_formation, -# inlist_star2_formation, -# star_history_columns, -# binary_history_columns, -# profile_columns, -# run_directory, -# grid_type, -# path_to_run_grid_exec, -# psycris_inifile) -# return command_line - -# Define column types and their filenames -column_types = ['star_history_columns', 'binary_history_columns', 'profile_columns'] -column_filenames = ['history_columns.list', 'binary_history_columns.list', 'profile_columns.list'] - -# Define extras keys -extras_keys = ['makefile_binary', 'makefile_star', 'binary_run' - 'star_run', 'binary_extras', 'star_binary_extras', 'star1_extras', 'star2_extras',] - - -############################################################################### -# BEGIN MAIN FUNCTION -############################################################################### -if __name__ == '__main__': - - # READ COMMANDLINE ARGUMENTS - ########################################################################### args = parse_commandline() + run_setup(args) - # Setup logging based on verbosity level - setup_logger(args.verbose) - try: - os.environ['MESA_DIR'] - except: - raise ValueError("MESA_DIR must be defined in your environment " - "before you can run a grid of MESA runs") - - # check if given file exists - if not os.path.isfile(args.inifile): - raise FileNotFoundError("The provided inifile does not exist, please check the path and try again") - - # Determine location of executables - proc = subprocess.Popen(['which', 'posydon-run-grid'], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE - ) - (path_to_run_grid_exec, err) = proc.communicate() - if not path_to_run_grid_exec: - raise ValueError('Cannot locate posydon-run-grid executable in your path') - else: - path_to_run_grid_exec = path_to_run_grid_exec.decode('utf-8').strip('\n') - - run_parameters, slurm, user_mesa_inlists, user_mesa_extras = configfile.parse_inifile(args.inifile) - - # Add default values for run parameters if not provided - # - # move to separate function in this file to setup these defaults - if 'keep_profiles' not in run_parameters.keys(): - run_parameters['keep_profiles'] = False - if 'keep_photos' not in run_parameters.keys(): - run_parameters['keep_photos'] = False - - if ((not os.path.isfile(run_parameters['grid'])) and (not os.path.isdir(run_parameters['grid']))): - raise ValueError("Supplied grid does not exist, please check your path and try again") - - if ('psycris_inifile' not in run_parameters.keys()) and (args.grid_type == 'dynamic'): - raise ValueError("Please add psycris inifile to the [run_parameters] section of the inifile.") - - # Check if a base is provided for the run - if ('base' not in user_mesa_inlists.keys() - or user_mesa_inlists['base'] is None - or user_mesa_inlists['base'] == ""): - raise ValueError("Please provide a base for the MESA run in the configuration file") - - logger.debug(f'Base provided in inifile:\n{user_mesa_inlists["base"]}') - - # setup the inlist repository - # MESA_version_base_path is the path to the base of the version in the inlist repository - MESA_version_root_path = setup_inlist_repository(user_mesa_inlists.get('inlist_repository', None), - user_mesa_inlists['mesa_version']) - - # Setup the MESA_default, which is always needed - MESA_default_inlists, \ - MESA_default_extras, \ - MESA_default_columns = setup_MESA_defaults(MESA_version_root_path) - - # Setup POSYDON configuration (handles MESA base internally) - POSYDON_inlists, \ - POSYDON_extras, \ - POSYDON_columns = setup_POSYDON(MESA_version_root_path, - user_mesa_inlists['base'], - user_mesa_inlists['system_type']) - - # extract user inlists, extras and columns - user_inlists, \ - user_extras, \ - user_columns = setup_user(user_mesa_inlists, - user_mesa_extras) - - # build the final columns dictionary - final_columns = resolve_columns(MESA_default_columns, - POSYDON_columns, - user_columns) - final_extras = resolve_extras(MESA_default_extras, - POSYDON_extras, - user_extras) - - # Read grid to get grid parameters - nr_systems, grid_parameters, fixgrid_file_name = read_grid_file(run_parameters['grid']) - - # Extract output settings from user configuration - user_output_settings = get_additional_user_settings(mesa_inlists=user_mesa_inlists) - - # Stack all inlist layers together: MESA → POSYDON → user → grid → output - # This returns a dictionary of inlist parameters and their final values - final_inlists = resolve_inlists(MESA_default_inlists, - POSYDON_inlists, - user_inlists, - grid_parameters=grid_parameters, - output_settings=user_output_settings, - system_type=user_mesa_inlists['system_type']) - - # Setup the run directory with all necessary files - # This also creates the inlists for the grid run - output_paths = setup_grid_run_folder(args.run_directory, - final_columns, - final_extras, - final_inlists) - - # Now we can create the mpi command line to run the grid - if slurm['job_array']: - command_line = construct_command_line(1, - fixgrid_file_name, - output_paths['binary_executable'], - output_paths['star1_executable'], - output_paths['star2_executable'], - output_paths['inlist_binary_project'], - output_paths['inlist_star1_binary'], - output_paths['inlist_star2_binary'], - None, # inlist_star1_formation - None, # inlist_star2_formation, - output_paths['star_history_columns'], - output_paths['binary_history_columns'], - output_paths['profile_columns'], - args.run_directory, - 'fixed', - path_to_run_grid_exec, - keep_profiles=run_parameters['keep_profiles'], - keep_photos=run_parameters['keep_photos'] - ) - command_line += ' --grid-point-index $SLURM_ARRAY_TASK_ID' - - if args.submission_type == 'slurm': - command_line += ' --job_end $SLURM_JOB_END_TIME' - if 'work_dir' in slurm.keys() and not(slurm['work_dir'] == ''): - command_line += ' --temporary-directory '+slurm['work_dir'] - - - # if args.grid_type == "dynamic": - # dynamic_grid_params = parse_inifile(run_parameters["psycris_inifile"]) - # mesa_params_to_run_grid_over = dynamic_grid_params["posydon_dynamic_sampling_kwargs"]["mesa_column_names"] - # inlist_star1_formation, inlist_star2_formation, inlist_binary_project, inlist_star1_binary, \ - # inlist_star2_binary = construct_static_inlist(mesa_inlists, - # grid_parameters=mesa_params_to_run_grid_over, - # working_directory=args.run_directory) - - # now we can write the mpi command line - # if slurm['job_array']: - # command_line = construct_command_line(1, - # run_parameters['grid'], - # binary_exe, - # star1_exe, - # star2_exe, - # inlist_binary_project, - # inlist_star1_binary, - # inlist_star2_binary, - # inlist_star1_formation, - # inlist_star2_formation, - # star_history_columns, - # binary_history_columns, - # profile_columns, - # args.run_directory, - # 'fixed', - # path_to_run_grid_exec, - # keep_profiles=run_parameters['keep_profiles'], - # keep_photos=run_parameters['keep_photos']) - # command_line += ' --grid-point-index $SLURM_ARRAY_TASK_ID' - # else: - # command_line = construct_command_line(slurm['number_of_mpi_tasks']*slurm['number_of_nodes'], - # fixgrid_file_name, - # binary_exe, - # star1_exe, - # star2_exe, - # inlist_binary_project, - # inlist_star1_binary, - # inlist_star2_binary, - # inlist_star1_formation, - # inlist_star2_formation, - # star_history_columns, - # binary_history_columns, - # profile_columns, - # args.run_directory, - # args.grid_type, - # path_to_run_grid_exec, - # psycris_inifile = run_parameters["psycris_inifile"], - # keep_profiles=run_parameters['keep_profiles'], - # # keep_photos=run_parameters['keep_photos']) - - if 'work_dir' in slurm.keys() and slurm['work_dir']: - command_line += f' --temporary-directory {slurm["work_dir"]}' - - # Generate submission scripts - generate_submission_scripts(args.submission_type, command_line, slurm, nr_systems) - logger.info("Setup complete! You can now submit your grid to the cluster.") - - if args.submission_type == 'slurm': - logger.info("To submit, run the following command:") - logger.info(f" sbatch submit_slurm.sh") +if __name__ == '__main__': + main() diff --git a/posydon/CLI/grids/__init__.py b/posydon/CLI/grids/__init__.py index e69de29bb2..ae05b9a79b 100644 --- a/posydon/CLI/grids/__init__.py +++ b/posydon/CLI/grids/__init__.py @@ -0,0 +1,8 @@ +"""POSYDON CLI grids module. + +This module provides CLI tools for managing POSYDON MESA grids. +""" + +from posydon.CLI.grids.setup import run_setup + +__all__ = ['run_setup'] diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index 5c840c3c4f..22c589a310 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -1885,3 +1885,388 @@ def construct_command_line(number_of_mpi_processes, path_to_grid, path_to_run_grid_exec, psycris_inifile) return command_line + + +############################################################################### +# VALIDATION AND ENVIRONMENT HELPERS +############################################################################### + +def validate_environment(): + """Validate that required environment variables are set. + + Raises + ------ + ValueError + If MESA_DIR is not set in the environment + """ + if 'MESA_DIR' not in os.environ: + raise ValueError( + "MESA_DIR must be defined in your environment " + "before you can run a grid of MESA runs" + ) + + +def find_run_grid_executable(): + """Find the posydon-run-grid executable in the system PATH. + + Returns + ------- + str + Path to the posydon-run-grid executable + + Raises + ------ + ValueError + If the executable cannot be found + """ + proc = subprocess.Popen( + ['which', 'posydon-run-grid'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + (path_to_exec, err) = proc.communicate() + + if not path_to_exec: + raise ValueError('Cannot locate posydon-run-grid executable in your path') + + return path_to_exec.decode('utf-8').strip('\n') + + +def validate_inifile(inifile_path): + """Validate that the inifile exists. + + Parameters + ---------- + inifile_path : str + Path to the inifile + + Raises + ------ + FileNotFoundError + If the inifile does not exist + """ + if not os.path.isfile(inifile_path): + raise FileNotFoundError( + "The provided inifile does not exist, please check the path and try again" + ) + + +def validate_and_setup_run_parameters(run_parameters, grid_type): + """Validate run parameters and set defaults. + + Parameters + ---------- + run_parameters : dict + Dictionary of run parameters from the inifile + grid_type : str + Type of grid ('fixed' or 'dynamic') + + Returns + ------- + dict + Updated run parameters with defaults applied + + Raises + ------ + ValueError + If required parameters are missing or invalid + """ + # Set defaults + if 'keep_profiles' not in run_parameters: + run_parameters['keep_profiles'] = False + + if 'keep_photos' not in run_parameters: + run_parameters['keep_photos'] = False + + # Validate grid path + grid_path = run_parameters.get('grid') + if grid_path is None or (not os.path.isfile(grid_path) and not os.path.isdir(grid_path)): + raise ValueError( + "Supplied grid does not exist, please check your path and try again" + ) + + # Validate dynamic grid requirements + if grid_type == 'dynamic' and 'psycris_inifile' not in run_parameters: + raise ValueError( + "Please add psycris inifile to the [run_parameters] section of the inifile." + ) + + return run_parameters + + +def validate_mesa_inlists(user_mesa_inlists): + """Validate user MESA inlist configuration. + + Parameters + ---------- + user_mesa_inlists : dict + Dictionary of user MESA inlist settings + + Raises + ------ + ValueError + If required settings are missing + """ + if ('base' not in user_mesa_inlists + or user_mesa_inlists['base'] is None + or user_mesa_inlists['base'] == ""): + raise ValueError( + "Please provide a base for the MESA run in the configuration file" + ) + + +############################################################################### +# CONFIGURATION BUILDING +############################################################################### + +def build_configuration_stack(user_mesa_inlists, user_mesa_extras, run_parameters): + """Build the complete configuration stack for the MESA grid. + + This function orchestrates the layering of configurations: + MESA defaults → POSYDON defaults → User settings → Grid parameters + + Parameters + ---------- + user_mesa_inlists : dict + User MESA inlist settings from the inifile + user_mesa_extras : dict + User MESA extras settings from the inifile + run_parameters : dict + Run parameters from the inifile + + Returns + ------- + tuple + (final_columns, final_extras, final_inlists, nr_systems, grid_parameters, fixgrid_file_name) + """ + # Setup the inlist repository + MESA_version_root_path = setup_inlist_repository( + user_mesa_inlists.get('inlist_repository', None), + user_mesa_inlists['mesa_version'] + ) + + # Setup MESA defaults (always needed) + MESA_default_inlists, \ + MESA_default_extras, \ + MESA_default_columns = setup_MESA_defaults(MESA_version_root_path) + + # Setup POSYDON configuration (handles MESA base internally) + POSYDON_inlists, \ + POSYDON_extras, \ + POSYDON_columns = setup_POSYDON( + MESA_version_root_path, + user_mesa_inlists['base'], + user_mesa_inlists['system_type'] + ) + + # Extract user inlists, extras and columns + user_inlists, \ + user_extras, \ + user_columns = setup_user(user_mesa_inlists, user_mesa_extras) + + # Build final columns dictionary + final_columns = resolve_columns( + MESA_default_columns, + POSYDON_columns, + user_columns + ) + + # Build final extras dictionary + final_extras = resolve_extras( + MESA_default_extras, + POSYDON_extras, + user_extras + ) + + # Read grid to get grid parameters + nr_systems, grid_parameters, fixgrid_file_name = read_grid_file(run_parameters['grid']) + + # Extract output settings from user configuration + user_output_settings = get_additional_user_settings(mesa_inlists=user_mesa_inlists) + + # Stack all inlist layers together + final_inlists = resolve_inlists( + MESA_default_inlists, + POSYDON_inlists, + user_inlists, + grid_parameters=grid_parameters, + output_settings=user_output_settings, + system_type=user_mesa_inlists['system_type'] + ) + + return final_columns, final_extras, final_inlists, nr_systems, grid_parameters, fixgrid_file_name + + +############################################################################### +# COMMAND LINE BUILDING +############################################################################### + +def build_command_line_for_grid(slurm, output_paths, fixgrid_file_name, + run_directory, run_parameters, + path_to_run_grid_exec, submission_type): + """Build the command line for running the grid. + + Parameters + ---------- + slurm : dict + SLURM configuration dictionary + output_paths : dict + Dictionary of output paths from setup_grid_run_folder + fixgrid_file_name : str + Path to the grid file + run_directory : str + Directory for output + run_parameters : dict + Run parameters from inifile + path_to_run_grid_exec : str + Path to the posydon-run-grid executable + submission_type : str + Type of submission ('shell' or 'slurm') + + Returns + ------- + str + The complete command line string + """ + if slurm['job_array']: + command_line = construct_command_line( + 1, + fixgrid_file_name, + output_paths['binary_executable'], + output_paths['star1_executable'], + output_paths['star2_executable'], + output_paths['inlist_binary_project'], + output_paths['inlist_star1_binary'], + output_paths['inlist_star2_binary'], + None, # inlist_star1_formation + None, # inlist_star2_formation + output_paths['star_history_columns'], + output_paths['binary_history_columns'], + output_paths['profile_columns'], + run_directory, + 'fixed', + path_to_run_grid_exec, + keep_profiles=run_parameters['keep_profiles'], + keep_photos=run_parameters['keep_photos'] + ) + command_line += ' --grid-point-index $SLURM_ARRAY_TASK_ID' + else: + # MPI mode (for future dynamic grids) + command_line = construct_command_line( + slurm.get('number_of_mpi_tasks', 1) * slurm.get('number_of_nodes', 1), + fixgrid_file_name, + output_paths['binary_executable'], + output_paths['star1_executable'], + output_paths['star2_executable'], + output_paths['inlist_binary_project'], + output_paths['inlist_star1_binary'], + output_paths['inlist_star2_binary'], + None, # inlist_star1_formation + None, # inlist_star2_formation + output_paths['star_history_columns'], + output_paths['binary_history_columns'], + output_paths['profile_columns'], + run_directory, + 'fixed', + path_to_run_grid_exec, + keep_profiles=run_parameters['keep_profiles'], + keep_photos=run_parameters['keep_photos'] + ) + + # Add SLURM-specific options + if submission_type == 'slurm': + command_line += ' --job_end $SLURM_JOB_END_TIME' + + # Add work directory if specified + if slurm.get('work_dir') and slurm['work_dir'] != '': + command_line += f' --temporary-directory {slurm["work_dir"]}' + + return command_line + + +############################################################################### +# MAIN SETUP ENTRY POINT +############################################################################### + +def run_setup(args): + """Main entry point for the grid setup process. + + This function orchestrates the entire setup workflow: + 1. Validate environment and inputs + 2. Parse configuration file + 3. Build configuration stack (MESA → POSYDON → User) + 4. Setup run directory with all necessary files + 5. Generate submission scripts + + Parameters + ---------- + args : argparse.Namespace + Parsed command-line arguments containing: + - inifile: Path to the configuration file + - grid_type: 'fixed' or 'dynamic' + - run_directory: Output directory + - submission_type: 'shell' or 'slurm' + - nproc: Number of processors + - verbose: Verbosity setting + """ + from posydon.utils import configfile + + # Setup logging based on verbosity level + setup_logger(args.verbose) + + # Validate environment + validate_environment() + + # Validate inifile exists + validate_inifile(args.inifile) + + # Find the run grid executable + path_to_run_grid_exec = find_run_grid_executable() + + # Parse the configuration file + run_parameters, slurm, user_mesa_inlists, user_mesa_extras = configfile.parse_inifile(args.inifile) + + # Validate and setup run parameters + run_parameters = validate_and_setup_run_parameters(run_parameters, args.grid_type) + + # Validate MESA inlist configuration + validate_mesa_inlists(user_mesa_inlists) + + logger.debug(f'Base provided in inifile:\n{user_mesa_inlists["base"]}') + + # Build the complete configuration stack + final_columns, final_extras, final_inlists, \ + nr_systems, grid_parameters, fixgrid_file_name = build_configuration_stack( + user_mesa_inlists, + user_mesa_extras, + run_parameters + ) + + # Setup the run directory with all necessary files + output_paths = setup_grid_run_folder( + args.run_directory, + final_columns, + final_extras, + final_inlists + ) + + # Build the command line + command_line = build_command_line_for_grid( + slurm, + output_paths, + fixgrid_file_name, + args.run_directory, + run_parameters, + path_to_run_grid_exec, + args.submission_type + ) + + # Generate submission scripts + generate_submission_scripts(args.submission_type, command_line, slurm, nr_systems) + + logger.info("Setup complete! You can now submit your grid to the cluster.") + + if args.submission_type == 'slurm': + logger.info("To submit, run the following command:") + logger.info(" sbatch submit_slurm.sh") From e6a643b7ff5c790789aa8bccdc25a56587c8c0c2 Mon Sep 17 00:00:00 2001 From: Max Briel Date: Tue, 27 Jan 2026 12:49:16 +0100 Subject: [PATCH 04/13] move single star logic to resolve_inlists --- posydon/CLI/grids/setup.py | 203 +++++++++++++++---------------------- 1 file changed, 82 insertions(+), 121 deletions(-) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index 22c589a310..a528931260 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -11,7 +11,6 @@ from posydon.grids.psygrid import PSyGrid from posydon.utils import configfile from posydon.utils import gridutils as utils -from posydon.utils.posydonwarning import Pwarn # Define column types and their filenames column_types = {'star_history_columns' :'history_columns.list', @@ -21,7 +20,7 @@ # Define extras keys extras_keys = ['makefile_binary', 'makefile_star', 'binary_run', - 'star_run', 'binary_extras', 'star_binary_extras', 'star1_extras', 'star2_extras',] + 'star_run', 'run_binary_extras', 'run_star_binary_extras', 'run_star1_extras', 'run_star2_extras',] # define inlist keys inlist_keys = ['binary_controls', 'binary_job', @@ -281,10 +280,10 @@ def mesa_path(module, *parts): 'binary_run.f') # Extras files for binary evolution - MESA_default_extras['binary_extras'] = mesa_path('binary', + MESA_default_extras['run_binary_extras'] = mesa_path('binary', 'src', 'run_binary_extras.f') - MESA_default_extras['star_binary_extras'] = mesa_path('binary', + MESA_default_extras['run_star_binary_extras'] = mesa_path('binary', 'src', 'run_star_extras.f') @@ -292,8 +291,8 @@ def mesa_path(module, *parts): # During binary evolution, star_binary_extras is used for both stars. # #Both stars use the same single-star module extras file star_extras_path = mesa_path('star', 'src', 'run_star_extras.f') - MESA_default_extras['star1_extras'] = star_extras_path - MESA_default_extras['star2_extras'] = star_extras_path + MESA_default_extras['run_star1_extras'] = star_extras_path + MESA_default_extras['run_star2_extras'] = star_extras_path # Verify all extras files exist for _, path in MESA_default_extras.items(): @@ -343,14 +342,17 @@ def setup_POSYDON(path_to_version, base, system_type): Dictionary of POSYDON column files paths """ # If user wants to use MESA base only, return empty dictionaries - if base == "MESA": + if base == "MESA" or base[0] == "MESA": POSYDON_columns = {name: None for name in column_types} POSYDON_inlists = {} POSYDON_extras = {key: None for key in extras_keys} return POSYDON_inlists, POSYDON_extras, POSYDON_columns # Setup POSYDON base path - POSYDON_path = os.path.join(path_to_version, base) + if len(base) == 2: + POSYDON_path = os.path.join(path_to_version, base[0], base[1]) + else: + POSYDON_path = os.path.join(path_to_version, base[0], base[1], base[2]) check_file_exist(POSYDON_path) logger.debug(f"Setting up POSYDON configuration: {POSYDON_path}") @@ -389,7 +391,8 @@ def setup_POSYDON(path_to_version, base, system_type): helium_star_inlist_step2 = os.path.join(POSYDON_path, 'single_HeMS', 'inlist_step2') - # We need to also include the HMS single star inlist to set up the single star evolution + # We need to also include the HMS single star inlist to set up + # the single star evolution single_star_inlist_path = os.path.join(POSYDON_path, 'single_HMS', 'single_star_inlist') @@ -419,13 +422,13 @@ def setup_POSYDON(path_to_version, base, system_type): #---------------------------------- POSYDON_extras = {} - POSYDON_extras['binary_extras'] = os.path.join(POSYDON_path, + POSYDON_extras['run_binary_extras'] = os.path.join(POSYDON_path, 'extras_files', 'run_binary_extras.f') - POSYDON_extras['star_binary_extras'] = os.path.join(POSYDON_path, + POSYDON_extras['run_star_binary_extras'] = os.path.join(POSYDON_path, 'extras_files', 'run_star_extras.f') - POSYDON_extras['star1_extras'] = os.path.join(POSYDON_path, + POSYDON_extras['run_star1_extras'] = os.path.join(POSYDON_path, 'extras_files', 'run_star_extras.f') @@ -758,93 +761,8 @@ def print_inlist_stacking_table(keys, MESA_defaults, POSYDON_config, user_config print(f"Note: Files at the top override parameters from files below") print(f"{MAGENTA}user{RESET} (highest priority) → {YELLOW}POSYDON{RESET} (config) → {CYAN}MESA{RESET} (base)") -def print_inlist_parameter_override_table(key, mesa_params, posydon_params, user_params, final_params, show_details=False): - """Print a table showing which layer each parameter comes from, similar to extras/columns tables. - - Parameters - ---------- - key : str - The inlist key (e.g., 'binary_controls', 'star1_controls') - mesa_params : dict - Parameters from MESA defaults - posydon_params : dict - Parameters from POSYDON config - user_params : dict - Parameters from user config - final_params : dict - Final merged parameters - show_details : bool, optional - If True, show detailed parameter-by-parameter table. Default is False. - """ - # Get all unique parameter names - all_params = sorted(set(list(mesa_params.keys()) + - list(posydon_params.keys()) + - list(user_params.keys()))) - - if not all_params: - return - - # Only show detailed table if requested - if not show_details: - return - # Find the longest parameter name for formatting - max_param_len = max(len(str(param)) for param in all_params) - max_param_len = max(max_param_len, 15) # Minimum width - col_width = 12 - - # Print summary header - overridden_count = sum(1 for p in all_params if (p in user_params and (p in mesa_params or p in posydon_params)) or - (p in posydon_params and p in mesa_params)) - print(f" {BOLD}Detailed Parameters:{RESET} {len(all_params)} total, {overridden_count} overridden") - print(" " + "=" * (max_param_len + col_width * 3 + 4)) - - # Print header with colored column names - header = f" {'Parameter':<{max_param_len}} {CYAN}{'MESA':^{col_width}}{RESET}{YELLOW}{'POSYDON':^{col_width}}{RESET}{MAGENTA}{'user':^{col_width}}{RESET}" - print(f"{BOLD}{header}{RESET}") - print(" " + "-" * (max_param_len + col_width * 3 + 4)) - - # Print each parameter row - for param in all_params: - # Check which configs have this parameter - has_mesa = param in mesa_params - has_posydon = param in posydon_params - has_user = param in user_params - - # Determine which one is used (priority: user > POSYDON > MESA) - used = 'user' if has_user else ('POSYDON' if has_posydon else ('MESA' if has_mesa else None)) - - # Format each column - mesa_mark = 'x' if has_mesa else ' ' - posydon_mark = 'x' if has_posydon else ' ' - user_mark = 'x' if has_user else ' ' - - # Apply colors - green for used, gray for available but not used - if used == 'MESA': - mesa_str = f"{GREEN}{mesa_mark}{RESET}" - posydon_str = f"{GRAY}{posydon_mark}{RESET}" - user_str = f"{GRAY}{user_mark}{RESET}" - elif used == 'POSYDON': - mesa_str = f"{GRAY}{mesa_mark}{RESET}" - posydon_str = f"{GREEN}{posydon_mark}{RESET}" - user_str = f"{GRAY}{user_mark}{RESET}" - elif used == 'user': - mesa_str = f"{GRAY}{mesa_mark}{RESET}" - posydon_str = f"{GRAY}{posydon_mark}{RESET}" - user_str = f"{GREEN}{user_mark}{RESET}" - else: - mesa_str = f"{GRAY}{mesa_mark}{RESET}" - posydon_str = f"{GRAY}{posydon_mark}{RESET}" - user_str = f"{GRAY}{user_mark}{RESET}" - - # Print row - print(f" {param:<{max_param_len}} {mesa_str:^{col_width+9}}{posydon_str:^{col_width+9}}{user_str:^{col_width+9}}") - - print(" " + "=" * (max_param_len + col_width * 3 + 4)) - print(f" {GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used") - - -def print_inlist_parameter_override_table_v2(key, layer_params, final_params, show_details=False): +def print_inlist_parameter_override_table(key, layer_params, final_params, show_details=False): """Log a table showing which layer each parameter comes from (supports all layers). Parameters @@ -1234,8 +1152,9 @@ def to_fortran_bool(value): return output_layer -def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_type, - grid_parameters=None, output_settings=None): +def resolve_inlists(MESA_default_inlists, POSYDON_inlists, + user_inlists, system_type, run_directory, grid_parameters=None, + output_settings=None): """Resolve final inlists to use based on priority: output_settings > grid_parameters > user_inlists > POSYDON_inlists > MESA_default_inlists @@ -1263,6 +1182,8 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ Dictionary of user inlists paths system_type : str Type of binary system + run_directory : str + Path to the run directory where inlist files will be created grid_parameters : list or set, optional Collection of grid parameter names. If provided, adds grid configuration layer. output_settings : dict, optional @@ -1287,7 +1208,8 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ 'POSYDON': {}, 'user': {}, 'grid': {}, - 'output': {} + 'output': {}, + 'inlist_names': {} } # Track layer parameters for detailed printing if requested @@ -1296,13 +1218,24 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ 'POSYDON': {}, 'user': {}, 'grid': {}, - 'output': {} + 'output': {}, + 'inlist_names': {} } # First pass: process file-based layers (MESA, POSYDON, user) for key in all_keys: # Determine the section based on the key name section = _get_section_from_key(key) + if 'single' in system_type and ('binary' in key or 'star2' in key): + # Skip binary or star2 sections for single star systems + final_inlists[key] = {} + layer_counts['MESA'][key] = 0 + layer_counts['POSYDON'][key] = 0 + layer_counts['user'][key] = 0 + layer_params['MESA'][key] = {} + layer_params['POSYDON'][key] = {} + layer_params['user'][key] = {} + continue # Process each file-based layer mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), section) @@ -1356,6 +1289,32 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ layer_counts['output'][key] = 0 layer_params['output'][key] = {} + # Add inlist_names layer for binary systems + # This must happen after all other layers to use the constructed run_directory paths + if 'single' not in system_type.lower(): + inlist_star1_binary = os.path.join(run_directory, 'binary', 'inlist1') + inlist_star2_binary = os.path.join(run_directory, 'binary', 'inlist2') + + inlist_names_params = { + 'inlist_names(1)': f"'{inlist_star1_binary}'", + 'inlist_names(2)': f"'{inlist_star2_binary}'" + } + final_inlists['binary_job'].update(inlist_names_params) + + # Track this layer + for key in all_keys: + if key == 'binary_job': + layer_counts['inlist_names'][key] = len(inlist_names_params) + layer_params['inlist_names'][key] = inlist_names_params + else: + layer_counts['inlist_names'][key] = 0 + layer_params['inlist_names'][key] = {} + else: + # Single star systems don't need inlist_names + for key in all_keys: + layer_counts['inlist_names'][key] = 0 + layer_params['inlist_names'][key] = {} + # Log at INFO level: Parameter count summary print_inlist_summary_table_v2(all_keys, layer_counts) @@ -1365,7 +1324,7 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, user_inlists, system_ # Only show sections that have parameters in any layer if any(layer_params[layer][key] for layer in layer_params): logger.debug(f"{BOLD}═══ {key} ═══{RESET}") - print_inlist_parameter_override_table_v2( + print_inlist_parameter_override_table( key, layer_params, final_inlists[key], @@ -1584,15 +1543,14 @@ def _copy_extras(path, final_extras): 'binary_run': [('binary/src', 'binary_run.f')], 'star_run': [('star1/src', 'run.f'), ('star2/src', 'run.f')], - 'binary_extras': [('binary/src', 'run_binary_extras.f')], - 'star_binary_extras': [('binary/src', 'run_star_extras.f')], - 'star1_extras': [('star1/src', 'run_star_extras.f')], - 'star2_extras': [('star2/src', 'run_star_extras.f')], + 'run_binary_extras': [('binary/src', 'run_binary_extras.f')], + 'run_star_binary_extras': [('binary/src', 'run_star_extras.f')], + 'run_star1_extras': [('star1/src', 'run_star_extras.f')], + 'run_star2_extras': [('star2/src', 'run_star_extras.f')], } for key, value in final_extras.items(): logger.debug(f"{key}: {value}") - if key == 'mesa_dir': continue @@ -1605,7 +1563,8 @@ def _copy_extras(path, final_extras): shutil.copy(value, path) -def setup_grid_run_folder(path, final_columns, final_extras, final_inlists): +def setup_grid_run_folder(path, final_columns, final_extras, + final_inlists): """Set up the grid run folder by: 1. Creating necessary subdirectories @@ -1649,9 +1608,8 @@ def setup_grid_run_folder(path, final_columns, final_extras, final_inlists): inlist_star1_binary = os.path.join(path, 'binary', 'inlist1') inlist_star2_binary = os.path.join(path, 'binary', 'inlist2') - # Add inlist names to binary_job section - final_inlists['binary_job']['inlist_names(1)'] = f"'{inlist_star1_binary}'" - final_inlists['binary_job']['inlist_names(2)'] = f"'{inlist_star2_binary}'" + # inlist_names(1) and inlist_names(2) are now set in resolve_inlists + # for binary systems, so no need to add them here # Write all three inlist files _write_binary_inlist(inlist_binary_project, @@ -1807,7 +1765,7 @@ def generate_submission_scripts(submission_type, command_line, slurm, nr_systems # Generate main grid submission script grid_script = 'job_array_grid_submit.slurm' if slurm['job_array'] else 'mpi_grid_submit.slurm' if os.path.exists(grid_script): - Pwarn(f'Replace {grid_script}', "OverwriteWarning") + logger.warning(f'Replace {grid_script}') array_size = nr_systems if slurm['job_array'] else None with open(grid_script, 'w') as f: @@ -1818,7 +1776,7 @@ def generate_submission_scripts(submission_type, command_line, slurm, nr_systems # Generate cleanup script cleanup_script = 'cleanup.slurm' if os.path.exists(cleanup_script): - Pwarn(f'Replace {cleanup_script}', "OverwriteWarning") + logger.warning(f'Replace {cleanup_script}') with open(cleanup_script, 'w') as f: _write_sbatch_header(f, slurm, job_type='cleanup') @@ -1827,8 +1785,7 @@ def generate_submission_scripts(submission_type, command_line, slurm, nr_systems # Generate wrapper run script run_script = 'run_grid.sh' if os.path.exists(run_script): - Pwarn(f'Replace {run_script}', "OverwriteWarning") - + logger.warning(f'Replace {run_script}') with open(run_script, 'w') as f: f.write('#!/bin/bash\n') f.write(f'ID_GRID=$(sbatch --parsable {grid_script})\n') @@ -1838,7 +1795,7 @@ def generate_submission_scripts(submission_type, command_line, slurm, nr_systems f.write('echo "cleanup.slurm submitted as "${ID_cleanup}\n') os.system(f"chmod 755 {run_script}") - print(f"Created {grid_script}, {cleanup_script}, and {run_script}") + logger.info(f"Created {grid_script}, {cleanup_script}, and {run_script}") def construct_command_line(number_of_mpi_processes, path_to_grid, @@ -2020,7 +1977,7 @@ def validate_mesa_inlists(user_mesa_inlists): # CONFIGURATION BUILDING ############################################################################### -def build_configuration_stack(user_mesa_inlists, user_mesa_extras, run_parameters): +def build_configuration_stack(user_mesa_inlists, user_mesa_extras, run_parameters, run_directory): """Build the complete configuration stack for the MESA grid. This function orchestrates the layering of configurations: @@ -2034,6 +1991,8 @@ def build_configuration_stack(user_mesa_inlists, user_mesa_extras, run_parameter User MESA extras settings from the inifile run_parameters : dict Run parameters from the inifile + run_directory : str + Path to the run directory where files will be created Returns ------- @@ -2090,9 +2049,10 @@ def build_configuration_stack(user_mesa_inlists, user_mesa_extras, run_parameter MESA_default_inlists, POSYDON_inlists, user_inlists, + system_type=user_mesa_inlists['system_type'], + run_directory=run_directory, grid_parameters=grid_parameters, - output_settings=user_output_settings, - system_type=user_mesa_inlists['system_type'] + output_settings=user_output_settings ) return final_columns, final_extras, final_inlists, nr_systems, grid_parameters, fixgrid_file_name @@ -2240,7 +2200,8 @@ def run_setup(args): nr_systems, grid_parameters, fixgrid_file_name = build_configuration_stack( user_mesa_inlists, user_mesa_extras, - run_parameters + run_parameters, + args.run_directory ) # Setup the run directory with all necessary files From f21f592de244029a15894a178f0342d63fb77c3a Mon Sep 17 00:00:00 2001 From: Max Briel Date: Tue, 27 Jan 2026 17:02:40 +0100 Subject: [PATCH 05/13] add single_HMS and mutli-step support --- posydon/CLI/grids/setup.py | 324 +++++++++++++++++++++++-------------- 1 file changed, 207 insertions(+), 117 deletions(-) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index a528931260..aed10cecec 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -24,6 +24,8 @@ # define inlist keys inlist_keys = ['binary_controls', 'binary_job', + 'binary_star1_controls', 'binary_star1_job', + 'binary_star2_controls', 'binary_star2_job', 'star1_controls', 'star1_job', 'star2_controls', 'star2_job'] @@ -253,6 +255,19 @@ def setup_MESA_defaults(path_to_version): MESA_default_inlists['star2_job'] = [os.path.join(MESA_defaults_inlists_path, 'star', 'star_job.defaults')] + MESA_default_inlists['binary_star1_controls'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'controls.defaults')] + MESA_default_inlists['binary_star1_job'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'star_job.defaults')] + MESA_default_inlists['binary_star2_controls'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'controls.defaults')] + MESA_default_inlists['binary_star2_job'] = [os.path.join(MESA_defaults_inlists_path, + 'star', + 'star_job.defaults')] + #---------------------------------- # EXTRAS @@ -361,61 +376,78 @@ def setup_POSYDON(path_to_version, base, system_type): # Inlists #---------------------------------- POSYDON_inlists = {} - # Common inlists - # TODOL these are not all needed for single stars. - common_inlists_path = os.path.join(POSYDON_path, 'common_inlists') - POSYDON_inlists['binary_controls'] = [os.path.join(common_inlists_path, 'inlist_project')] - POSYDON_inlists['binary_job'] = [os.path.join(common_inlists_path, 'inlist_project')] - # setup star1 inlists for binaries - POSYDON_inlists['star1_controls'] = [os.path.join(common_inlists_path, 'inlist1')] - POSYDON_inlists['star1_job'] = [os.path.join(common_inlists_path, 'inlist1')] - # setup star2 inlists for binaries - POSYDON_inlists['star2_controls'] = [os.path.join(common_inlists_path, 'inlist2')] - POSYDON_inlists['star2_job'] = [os.path.join(common_inlists_path, 'inlist2')] - + common_inlists_path = os.path.join(POSYDON_path, 'common_inlists') + # Single star inlists if system_type == 'single_HMS': + # setup star1 inlists + POSYDON_inlists['star1_controls'] = [[os.path.join(common_inlists_path, 'inlist1')]] + POSYDON_inlists['star1_job'] = [[os.path.join(common_inlists_path, 'inlist1')]] # setup the paths to extra single star inlists single_star_inlist_path = os.path.join(POSYDON_path, 'single_HMS', 'single_star_inlist') - POSYDON_inlists['star1_controls'].append(single_star_inlist_path) - elif system_type == 'single_HeMS': - # setup the paths to extra single star He inlists - # The HeMS single star inlists have two steps - # step 1: create HeMS star - # step 2: evolve HeMS star - helium_star_inlist_step1 = os.path.join(POSYDON_path, - 'single_HeMS', - 'inlist_step1') - helium_star_inlist_step2 = os.path.join(POSYDON_path, - 'single_HeMS', - 'inlist_step2') - # We need to also include the HMS single star inlist to set up - # the single star evolution - single_star_inlist_path = os.path.join(POSYDON_path, - 'single_HMS', - 'single_star_inlist') - - single_helium_inlists = [helium_star_inlist_step1, - helium_star_inlist_step2, - single_star_inlist_path] - - # the helium star setup steps contain control & job in the same inlist files - POSYDON_inlists['star1_controls'].extend(single_helium_inlists) - # We don't need to add the single star inlist again for the hob, - # since it only contains controls section in the file - POSYDON_inlists['star1_job'].extend(single_helium_inlists) - - elif system_type in ['HMS-HeMS', 'HeMS-HeMS']: - pass - elif system_type in ['CO-HMS', 'CO-HeMS']: - pass - elif system_type in ['HMS-HMS']: - # the common inlists are sufficient - pass - else: - raise ValueError(f"System type {system_type} not recognized.") + POSYDON_inlists['star1_controls'][0].append(single_star_inlist_path) + + elif system_type == 'HMS-HMS': + # setup the paths to common binary inlists + POSYDON_inlists['binary_controls'] = [[os.path.join(common_inlists_path, 'inlist_project')]] + POSYDON_inlists['binary_job'] = [[os.path.join(common_inlists_path, 'inlist_project')]] + # setup star1 inlists for binaries + POSYDON_inlists['binary_star1_controls'] = [[os.path.join(common_inlists_path, 'inlist1')]] + POSYDON_inlists['binary_star1_job'] = [[os.path.join(common_inlists_path, 'inlist1')]] + # setup star2 inlists for binaries + POSYDON_inlists['binary_star2_controls'] = [[os.path.join(common_inlists_path, 'inlist2')]] + POSYDON_inlists['binary_star2_job'] = [[os.path.join(common_inlists_path, 'inlist2')]] + + POSYDON_inlists['star1_controls'] = [[]] + POSYDON_inlists['star1_job'] = [[]] + + POSYDON_inlists['star2_controls'] = [[]] + POSYDON_inlists['star2_job'] = [[]] + + # if system_type == 'single_HMS': + # # setup the paths to extra single star inlists + # single_star_inlist_path = os.path.join(POSYDON_path, + # 'single_HMS', + # 'single_star_inlist') + # POSYDON_inlists['star1_controls'].append(single_star_inlist_path) + # elif system_type == 'single_HeMS': + # # setup the paths to extra single star He inlists + # # The HeMS single star inlists have two steps + # # step 1: create HeMS star + # # step 2: evolve HeMS star + # helium_star_inlist_step1 = os.path.join(POSYDON_path, + # 'single_HeMS', + # 'inlist_step1') + # helium_star_inlist_step2 = os.path.join(POSYDON_path, + # 'single_HeMS', + # 'inlist_step2') + # # We need to also include the HMS single star inlist to set up + # # the single star evolution + # single_star_inlist_path = os.path.join(POSYDON_path, + # 'single_HMS', + # 'single_star_inlist') + + # single_helium_inlists = [helium_star_inlist_step1, + # helium_star_inlist_step2, + # single_star_inlist_path] + + # # the helium star setup steps contain control & job in the same inlist files + # POSYDON_inlists['star1_controls'].extend(single_helium_inlists) + # # We don't need to add the single star inlist again for the hob, + # # since it only contains controls section in the file + # POSYDON_inlists['star1_job'].extend(single_helium_inlists) + + # elif system_type in ['HMS-HeMS', 'HeMS-HeMS']: + # pass + # elif system_type in ['CO-HMS', 'CO-HeMS']: + # pass + # elif system_type in ['HMS-HMS']: + # # the common inlists are sufficient + # pass + # else: + # raise ValueError(f"System type {system_type} not recognized.") #---------------------------------- # Extras @@ -476,9 +508,9 @@ def setup_user(user_mesa_inlists, user_mesa_extras): user_inlists = {} for key in inlist_keys: if key not in user_mesa_inlists.keys(): - user_inlists[key] = [] + user_inlists[key] = [[]] else: - user_inlists[key] = [user_mesa_inlists[key]] + user_inlists[key] = [[user_mesa_inlists[key]]] check_file_exist(user_mesa_inlists[key]) #---------------------------------- @@ -550,7 +582,6 @@ def resolve_configuration(keys, MESA_defaults, POSYDON_config, user_config, titl return final_config - def resolve_columns(MESA_default_columns, POSYDON_columns, user_columns): """Resolve final columns to use based on priority: user_columns > POSYDON_columns > MESA_default_columns @@ -585,7 +616,6 @@ def resolve_columns(MESA_default_columns, POSYDON_columns, user_columns): return final_columns - def resolve_extras(MESA_default_extras, POSYDON_extras, user_extras): """Resolve final extras to use based on priority: user_extras > POSYDON_extras > MESA_default_extras @@ -620,7 +650,6 @@ def resolve_extras(MESA_default_extras, POSYDON_extras, user_extras): return final_extras - def print_priority_table(keys, MESA_defaults, POSYDON_config, user_config, final_config, title="Configuration Priority"): """Log a visual table showing which configuration layer is used for each key. @@ -761,7 +790,6 @@ def print_inlist_stacking_table(keys, MESA_defaults, POSYDON_config, user_config print(f"Note: Files at the top override parameters from files below") print(f"{MAGENTA}user{RESET} (highest priority) → {YELLOW}POSYDON{RESET} (config) → {CYAN}MESA{RESET} (base)") - def print_inlist_parameter_override_table(key, layer_params, final_params, show_details=False): """Log a table showing which layer each parameter comes from (supports all layers). @@ -871,9 +899,6 @@ def print_inlist_parameter_override_table(key, layer_params, final_params, show_ logger.debug(" " + "=" * total_width) logger.debug(f" {GREEN}Green{RESET} = used, {GRAY}Gray{RESET} = available but not used") - - - def print_inlist_summary_table_v2(all_keys, layer_counts): """Log a summary table showing parameter counts per section at each layer. @@ -1045,6 +1070,18 @@ def _build_grid_parameter_layer(grid_parameters, final_inlists): 'star2_job': ('read_extra_star_job_inlist1', 'extra_star_job_inlist1_name', 'inlist_grid_star2_job'), + 'binary_star1_controls': ('read_extra_controls_inlist1', + 'extra_controls_inlist1_name', + 'inlist_grid_star1_binary_controls'), + 'binary_star2_controls': ('read_extra_controls_inlist1', + 'extra_controls_inlist1_name', + 'inlist_grid_star2_binary_controls'), + 'binary_star1_job': ('read_extra_star_job_inlist1', + 'extra_star_job_inlist1_name', + 'inlist_grid_star1_binary_job'), + 'binary_star2_job': ('read_extra_star_job_inlist1', + 'extra_star_job_inlist1_name', + 'inlist_grid_star2_binary_job'), } # Check which sections have grid parameters @@ -1091,24 +1128,28 @@ def to_fortran_bool(value): output_layer = { 'binary_controls': {}, 'binary_job': {}, - 'star1_controls': {}, + 'binary_star1_controls': {}, + 'binary_star1_job': {}, + 'binary_star2_controls': {}, + 'binary_star2_job': {}, 'star1_job': {}, + 'star1_controls': {}, + 'star2_job': {}, 'star2_controls': {}, - 'star2_job': {} } # Configuration: (config_key, section, enabled_param, filename_param, filename_value) output_config = [ - ('final_profile_star1', 'star1_job', 'write_profile_when_terminate', + ('final_profile_star1', 'binary_star1_job', 'write_profile_when_terminate', 'filename_for_profile_when_terminate', "'final_profile_star1.data'"), - ('final_profile_star2', 'star2_job', 'write_profile_when_terminate', + ('final_profile_star2', 'binary_star2_job', 'write_profile_when_terminate', 'filename_for_profile_when_terminate', "'final_profile_star2.data'"), - ('final_model_star1', 'star1_job', 'save_model_when_terminate', + ('final_model_star1', 'binary_star1_job', 'save_model_when_terminate', 'save_model_filename', "'final_star1.mod'"), - ('final_model_star2', 'star2_job', 'save_model_when_terminate', + ('final_model_star2', 'binary_star2_job', 'save_model_when_terminate', 'save_model_filename', "'final_star2.mod'"), - ('history_star1', 'star1_controls', 'do_history_file', None, None), - ('history_star2', 'star2_controls', 'do_history_file', None, None), + ('history_star1', 'binary_star1_controls', 'do_history_file', None, None), + ('history_star2', 'binary_star2_controls', 'do_history_file', None, None), ] # Process each output configuration @@ -1125,8 +1166,8 @@ def to_fortran_bool(value): if 'history_interval' in output_settings: interval = output_settings['history_interval'] output_layer['binary_controls']['history_interval'] = interval - output_layer['star1_controls']['history_interval'] = interval - output_layer['star2_controls']['history_interval'] = interval + output_layer['binary_star1_controls']['history_interval'] = interval + output_layer['binary_star2_controls']['history_interval'] = interval # Disable binary history if requested if 'binary_history' in output_settings and not output_settings['binary_history']: @@ -1142,13 +1183,6 @@ def to_fortran_bool(value): param_list = ', '.join(params.keys()) logger.debug(f" {section}: {param_list}") - # Handle ZAMS filenames if provided - if 'zams_filename_1' in output_settings and output_settings['zams_filename_1'] is not None: - output_layer['star1_controls']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" - - if 'zams_filename_2' in output_settings and output_settings['zams_filename_2'] is not None: - output_layer['star2_controls']['zams_filename'] = f"'{output_settings['zams_filename_2']}'" - return output_layer @@ -1222,40 +1256,62 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, 'inlist_names': {} } - # First pass: process file-based layers (MESA, POSYDON, user) - for key in all_keys: - # Determine the section based on the key name - section = _get_section_from_key(key) - if 'single' in system_type and ('binary' in key or 'star2' in key): - # Skip binary or star2 sections for single star systems - final_inlists[key] = {} - layer_counts['MESA'][key] = 0 - layer_counts['POSYDON'][key] = 0 - layer_counts['user'][key] = 0 - layer_params['MESA'][key] = {} - layer_params['POSYDON'][key] = {} - layer_params['user'][key] = {} - continue + print(all_keys) + nr_steps = 1 - # Process each file-based layer - mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), section) - posydon_layer_params = _process_inlist_layer(POSYDON_inlists.get(key), section) - user_layer_params = _process_inlist_layer(user_inlists.get(key), section) + if system_type == 'single_HMS': + # skip binary or star2 sections for single star systems + star1_keys = [key for key in all_keys if 'binary' not in key and 'star2' not in key] + + # set the other inlists as empty + for key in all_keys: + if key not in star1_keys: + final_inlists[key] = {} + + final_inlists['binary_star1_job'] = {'create_pre_main_sequence_model': ".false.", + 'load_saved_model': ".true.", + 'saved_model_name': "'initial_star1_step0.mod'"} + + for key in star1_keys: + section = _get_section_from_key(key) + mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), _get_section_from_key(key)) + + # only a single step for single star HMS systems + posydon_layer_params = _process_inlist_layer(POSYDON_inlists.get(key)[0], section) + user_layer_params = _process_inlist_layer(user_inlists.get(key)[0], section) - # Merge file-based layers (order matters: MESA first, then POSYDON, then user) - final_inlists[key] = {} - final_inlists[key].update(mesa_layer_params) - final_inlists[key].update(posydon_layer_params) - final_inlists[key].update(user_layer_params) + final_inlists[f'{key}_0'] = {} + final_inlists[f'{key}_0'].update(mesa_layer_params) + final_inlists[f'{key}_0'].update(posydon_layer_params) + final_inlists[f'{key}_0'].update(user_layer_params) - # Store counts and parameters for summary - layer_counts['MESA'][key] = len(mesa_layer_params) - layer_counts['POSYDON'][key] = len(posydon_layer_params) - layer_counts['user'][key] = len(user_layer_params) + # To the first step, add in the saving of the initial model + if 'star1_job' in key: + final_inlists[f'{key}_0']['save_model_when_terminate'] = '.true.' + final_inlists[f'{key}_0']['save_model_filename'] = "'initial_star1_step0.mod'" + + + elif system_type == "HMS-HMS": + # remove star job and controls for star1 if empty + HMS_HMS_keys = [key for key in all_keys if len(POSYDON_inlists.get(key, [])) > 0] + #First pass: process file-based layers (MESA, POSYDON, user) + for key in HMS_HMS_keys: + + # Determine the section based on the key name + section = _get_section_from_key(key) + + # Process each file-based layer + mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), section) + posydon_layer_params = _process_inlist_layer(POSYDON_inlists.get(key)[0], section) + user_layer_params = _process_inlist_layer(user_inlists.get(key)[0], section) + + # Merge file-based layers (order matters: MESA first, then POSYDON, then user) + final_inlists[key] = {} + final_inlists[key].update(mesa_layer_params) + final_inlists[key].update(posydon_layer_params) + final_inlists[key].update(user_layer_params) - layer_params['MESA'][key] = mesa_layer_params - layer_params['POSYDON'][key] = posydon_layer_params - layer_params['user'][key] = user_layer_params + all_keys = sorted(final_inlists.keys()) # Clean the final inlist parameters. # Needs to happen before adding grid/output layers! @@ -1279,6 +1335,7 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, # Build output controls layer if provided if output_settings: output_layer_dict = _build_output_controls_layer(output_settings) + for key in all_keys: output_params = output_layer_dict.get(key, {}) final_inlists[key].update(output_params) @@ -1289,6 +1346,23 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, layer_counts['output'][key] = 0 layer_params['output'][key] = {} + # Handle ZAMS filenames if provided + if output_settings and 'zams_filename_1' in output_settings and output_settings['zams_filename_1'] is not None: + final_inlists['binary_star1_controls']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" + layer_counts['output']['binary_star1_controls'] += 1 + layer_params['output']['binary_star1_controls']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" + + if output_settings and 'zams_filename_2' in output_settings and output_settings['zams_filename_2'] is not None: + final_inlists['binary_star2_controls']['zams_filename'] = f"'{output_settings['zams_filename_2']}'" + layer_counts['output']['binary_star2_controls'] += 1 + layer_params['output']['binary_star2_controls']['zams_filename'] = f"'{output_settings['zams_filename_2']}'" + + if system_type == 'single_HMS': + for i in range(nr_steps): + final_inlists[f'star1_controls_{i}']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" + layer_counts['output'][f'star1_controls_{i}'] = 1 + layer_params['output'][f'star1_controls_{i}'] = {'zams_filename': f"'{output_settings['zams_filename_1']}'"} + # Add inlist_names layer for binary systems # This must happen after all other layers to use the constructed run_directory paths if 'single' not in system_type.lower(): @@ -1603,28 +1677,44 @@ def setup_grid_run_folder(path, final_columns, final_extras, _create_build_script(path) # Write inlist files - logger.debug(f'{BOLD}Writing MESA inlist files:{RESET}') + logger.debug(f'{BOLD}Writing inlist files:{RESET}') inlist_binary_project = os.path.join(path, 'binary', 'inlist_project') inlist_star1_binary = os.path.join(path, 'binary', 'inlist1') inlist_star2_binary = os.path.join(path, 'binary', 'inlist2') - # inlist_names(1) and inlist_names(2) are now set in resolve_inlists - # for binary systems, so no need to add them here - - # Write all three inlist files + # Write all inlists _write_binary_inlist(inlist_binary_project, - final_inlists['binary_controls'], - final_inlists['binary_job']) + final_inlists['binary_controls'], + final_inlists['binary_job']) _write_star_inlist(inlist_star1_binary, - final_inlists['star1_controls'], - final_inlists['star1_job']) + final_inlists['binary_star1_controls'], + final_inlists['binary_star1_job']) _write_star_inlist(inlist_star2_binary, - final_inlists['star2_controls'], - final_inlists['star2_job']) + final_inlists['binary_star2_controls'], + final_inlists['binary_star2_job']) + + # check for additional single star inlists + # can be any number of steps + star1_steps = [key for key in final_inlists.keys() if 'star1_controls_' in key] + for step_key in star1_steps: + step_index = step_key.split('_')[-1] + inlists_star1 = os.path.join(path, 'star1', f'inlist_step{step_index}') + _write_star_inlist(inlists_star1, + final_inlists[f'star1_controls_{step_index}'], + final_inlists[f'star1_job_{step_index}']) + + + # single star inlists + #inlists_star1 = os.path.join(path, 'star1', 'inlist_step0') + #_write_star_inlist(inlists_star1, + # final_inlists['star1_controls'], + # final_inlists['star1_job']) + + - logger.info('MESA inlist files written successfully.') + logger.info('Inlist files written successfully.') # Essentials paths created in this functions output_paths = { From 8cda1bcb3350a47a79c8861db33c1eab1f149e5f Mon Sep 17 00:00:00 2001 From: Max Briel Date: Tue, 27 Jan 2026 17:42:49 +0100 Subject: [PATCH 06/13] add single_HeMS --- posydon/CLI/grids/setup.py | 109 +++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index aed10cecec..0f1e1a0f29 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -380,14 +380,40 @@ def setup_POSYDON(path_to_version, base, system_type): common_inlists_path = os.path.join(POSYDON_path, 'common_inlists') # Single star inlists if system_type == 'single_HMS': - # setup star1 inlists - POSYDON_inlists['star1_controls'] = [[os.path.join(common_inlists_path, 'inlist1')]] - POSYDON_inlists['star1_job'] = [[os.path.join(common_inlists_path, 'inlist1')]] # setup the paths to extra single star inlists single_star_inlist_path = os.path.join(POSYDON_path, 'single_HMS', 'single_star_inlist') - POSYDON_inlists['star1_controls'][0].append(single_star_inlist_path) + + POSYDON_inlists['star1_job'] = [[os.path.join(common_inlists_path, 'inlist1')]] + + POSYDON_inlists['star1_controls'] = [[os.path.join(common_inlists_path, 'inlist1'), + single_star_inlist_path]] + + elif system_type == 'single_HeMS': + single_star_inlist_path = os.path.join(POSYDON_path, + 'single_HMS', + 'single_star_inlist') + # helium star inlists + helium_star_inlists_folder = os.path.join(POSYDON_path, + 'single_HeMS') + # get steps + helium_star_inlists_steps = sorted(glob.glob(os.path.join(helium_star_inlists_folder, + 'inlist_step*'))) + # The HeMS single star inlists has "three" steps + POSYDON_inlists['star1_controls'] = [ + [os.path.join(common_inlists_path, 'inlist1'), helium_star_inlists_steps[0],], # step 0 + [os.path.join(common_inlists_path, 'inlist1'), helium_star_inlists_steps[1],], # step 1 + [os.path.join(common_inlists_path, 'inlist1'),single_star_inlist_path], # step 2 + + ] + POSYDON_inlists['star1_job'] = [ + [os.path.join(common_inlists_path, 'inlist1'), helium_star_inlists_steps[0],], # step 0 + [os.path.join(common_inlists_path, 'inlist1'), helium_star_inlists_steps[1],], # step 1 + [os.path.join(common_inlists_path, 'inlist1'), single_star_inlist_path], # step 2 + + ] + elif system_type == 'HMS-HMS': # setup the paths to common binary inlists @@ -1002,6 +1028,7 @@ def _process_inlist_layer(inlist_paths, section): layer_params = {} if inlist_paths: for file_path in inlist_paths: + print(file_path) inlist_dict = utils.clean_inlist_file(file_path, section=section)[section] layer_params.update(inlist_dict) return layer_params @@ -1268,10 +1295,6 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, if key not in star1_keys: final_inlists[key] = {} - final_inlists['binary_star1_job'] = {'create_pre_main_sequence_model': ".false.", - 'load_saved_model': ".true.", - 'saved_model_name': "'initial_star1_step0.mod'"} - for key in star1_keys: section = _get_section_from_key(key) mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), _get_section_from_key(key)) @@ -1285,10 +1308,57 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, final_inlists[f'{key}_0'].update(posydon_layer_params) final_inlists[f'{key}_0'].update(user_layer_params) - # To the first step, add in the saving of the initial model - if 'star1_job' in key: - final_inlists[f'{key}_0']['save_model_when_terminate'] = '.true.' - final_inlists[f'{key}_0']['save_model_filename'] = "'initial_star1_step0.mod'" + # To the first step, add in the saving of the initial model + final_inlists['star1_job_0']['save_model_when_terminate'] = '.true.' + final_inlists['star1_job_0']['save_model_filename'] = "'initial_star1_step0.mod'" + # TODO: can we remove this part? + # add specific to "save" the initial model loading in the binary star1 job + final_inlists['binary_star1_job'] = {'create_pre_main_sequence_model': ".false.", + 'load_saved_model': ".true.", + 'saved_model_name': "'initial_star1_step0.mod'"} + + + elif system_type == 'single_HeMS': + # skip binary or star2 sections for single star systems + star1_keys = [key for key in all_keys if 'binary' not in key and 'star2' not in key] + # set the other inlists as empty + for key in all_keys: + if key not in star1_keys: + final_inlists[key] = {} + + nr_steps = len(POSYDON_inlists.get('star1_controls', [])) + for i in range(nr_steps): + for key in star1_keys: + section = _get_section_from_key(key) + mesa_layer_params = _process_inlist_layer(MESA_default_inlists.get(key), _get_section_from_key(key)) + print(POSYDON_inlists.get(key)[0]) + posydon_layer_params = _process_inlist_layer(POSYDON_inlists.get(key)[i], section) + user_layer_params = _process_inlist_layer(user_inlists.get(key)[0], section) + + final_inlists[f'{key}_{i}'] = {} + final_inlists[f'{key}_{i}'].update(mesa_layer_params) + final_inlists[f'{key}_{i}'].update(posydon_layer_params) + final_inlists[f'{key}_{i}'].update(user_layer_params) + + + # To the first step, add in the saving of the initial model + final_inlists['star1_job_0']['save_model_when_terminate'] = '.true.' + final_inlists['star1_job_0']['save_model_filename'] = "'initial_star1_step0.mod'" + # Pass the model from one step to the next + for i in range(1, nr_steps): + final_inlists[f'star1_job_{i}']['create_pre_main_sequence_model'] = ".false." + final_inlists[f'star1_job_{i}']['load_saved_model'] = ".true." + final_inlists[f'star1_job_{i}']['saved_model_name'] = "'initial_star1_step{0}.mod'".format(i-1) + final_inlists[f'star1_job_{i}']['save_model_when_terminate'] = '.true.' + final_inlists[f'star1_job_{i}']['save_model_filename'] = "'initial_star1_step{0}.mod'".format(i) + + + # TODO: can we remove this part? + # add specific to "save" the initial model loading in the binary star1 job + final_inlists['binary_star1_job'] = {'create_pre_main_sequence_model': ".false.", + 'load_saved_model': ".true.", + 'saved_model_name': f"'initial_star1_step{nr_steps-1}.mod'"} + elif system_type == "HMS-HMS": @@ -1358,10 +1428,17 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, layer_params['output']['binary_star2_controls']['zams_filename'] = f"'{output_settings['zams_filename_2']}'" if system_type == 'single_HMS': - for i in range(nr_steps): - final_inlists[f'star1_controls_{i}']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" - layer_counts['output'][f'star1_controls_{i}'] = 1 - layer_params['output'][f'star1_controls_{i}'] = {'zams_filename': f"'{output_settings['zams_filename_1']}'"} + final_inlists[f'star1_controls_0']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" + layer_counts['output'][f'star1_controls_0'] = 1 + layer_params['output'][f'star1_controls_0'] = {'zams_filename': f"'{output_settings['zams_filename_1']}'"} + + if system_type == 'single_HeMS': + for i in range(0, nr_steps): + # remove zams filename from other steps + final_inlists[f'star1_controls_{i}'].pop('zams_filename', None) + # remove from binary + final_inlists['binary_star1_controls'].pop('zams_filename', None) + final_inlists['binary_star2_controls'].pop('zams_filename', None) # Add inlist_names layer for binary systems # This must happen after all other layers to use the constructed run_directory paths From 4db7f970b1d9cbed6a30b7b2dbf209d5b89c9413 Mon Sep 17 00:00:00 2001 From: Max Briel Date: Sun, 1 Feb 2026 19:24:44 +0100 Subject: [PATCH 07/13] other grids implementation --- posydon/CLI/grids/setup.py | 90 ++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index 0f1e1a0f29..b2bf66b4b2 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -404,7 +404,7 @@ def setup_POSYDON(path_to_version, base, system_type): POSYDON_inlists['star1_controls'] = [ [os.path.join(common_inlists_path, 'inlist1'), helium_star_inlists_steps[0],], # step 0 [os.path.join(common_inlists_path, 'inlist1'), helium_star_inlists_steps[1],], # step 1 - [os.path.join(common_inlists_path, 'inlist1'),single_star_inlist_path], # step 2 + [os.path.join(common_inlists_path, 'inlist1'), single_star_inlist_path], # step 2 ] POSYDON_inlists['star1_job'] = [ @@ -432,48 +432,36 @@ def setup_POSYDON(path_to_version, base, system_type): POSYDON_inlists['star2_controls'] = [[]] POSYDON_inlists['star2_job'] = [[]] - # if system_type == 'single_HMS': - # # setup the paths to extra single star inlists - # single_star_inlist_path = os.path.join(POSYDON_path, - # 'single_HMS', - # 'single_star_inlist') - # POSYDON_inlists['star1_controls'].append(single_star_inlist_path) - # elif system_type == 'single_HeMS': - # # setup the paths to extra single star He inlists - # # The HeMS single star inlists have two steps - # # step 1: create HeMS star - # # step 2: evolve HeMS star - # helium_star_inlist_step1 = os.path.join(POSYDON_path, - # 'single_HeMS', - # 'inlist_step1') - # helium_star_inlist_step2 = os.path.join(POSYDON_path, - # 'single_HeMS', - # 'inlist_step2') - # # We need to also include the HMS single star inlist to set up - # # the single star evolution - # single_star_inlist_path = os.path.join(POSYDON_path, - # 'single_HMS', - # 'single_star_inlist') - - # single_helium_inlists = [helium_star_inlist_step1, - # helium_star_inlist_step2, - # single_star_inlist_path] - - # # the helium star setup steps contain control & job in the same inlist files - # POSYDON_inlists['star1_controls'].extend(single_helium_inlists) - # # We don't need to add the single star inlist again for the hob, - # # since it only contains controls section in the file - # POSYDON_inlists['star1_job'].extend(single_helium_inlists) - - # elif system_type in ['HMS-HeMS', 'HeMS-HeMS']: - # pass - # elif system_type in ['CO-HMS', 'CO-HeMS']: - # pass - # elif system_type in ['HMS-HMS']: - # # the common inlists are sufficient - # pass - # else: - # raise ValueError(f"System type {system_type} not recognized.") + elif system_type == 'CO-HMS': + # Setup CO-HeMS inlist paths + common_project_inlist = os.path.join(common_inlists_path, 'inlist_project') + CO_HMS_inlists_path = os.path.join(POSYDON_path, 'CO-HMS') + CO_HMS_project_inlist = os.path.join(CO_HMS_inlists_path, 'inlist_project') + + POSYDON_inlists['binary_controls'] = [[common_project_inlist, CO_HMS_project_inlist]] + POSYDON_inlists['binary_job'] = [[common_project_inlist, CO_HMS_project_inlist]] + + # setup star1 inlists for binaries + POSYDON_inlists['binary_star1_controls'] = [[os.path.join(common_inlists_path, 'inlist1'), + os.path.join(CO_HMS_inlists_path, 'inlist1')]] + POSYDON_inlists['binary_star1_job'] = [[os.path.join(common_inlists_path, 'inlist1'), + os.path.join(CO_HMS_inlists_path, 'inlist1')]] + # setup star2 inlists for binaries + POSYDON_inlists['binary_star2_controls'] = [[os.path.join(common_inlists_path, 'inlist2'), + os.path.join(CO_HMS_inlists_path, 'inlist2')]] + POSYDON_inlists['binary_star2_job'] = [[os.path.join(common_inlists_path, 'inlist2'), + os.path.join(CO_HMS_inlists_path, 'inlist2')]] + + POSYDON_inlists['star1_controls'] = [[]] + POSYDON_inlists['star1_job'] = [[]] + POSYDON_inlists['star2_controls'] = [[]] + POSYDON_inlists['star2_job'] = [[]] + + elif system_type == 'CO-HeMS': + # Setup CO-HeMS inlist paths + common_project_inlist = os.path.join(common_inlists_path, 'inlist_project') + CO_HeMS_inlists_path = os.path.join(POSYDON_path, 'CO-HeMS') + #---------------------------------- # Extras @@ -1135,13 +1123,15 @@ def _build_grid_parameter_layer(grid_parameters, final_inlists): return grid_layer -def _build_output_controls_layer(output_settings): +def _build_output_controls_layer(output_settings, system_type): """Build a layer of inlist parameters for output control configurations. Parameters ---------- output_settings : dict Dictionary of output settings from the configuration file + system_type : str + Type of binary system Returns ------- @@ -1183,6 +1173,9 @@ def to_fortran_bool(value): for config_key, section, enabled_param, filename_param, filename_value in output_config: if config_key in output_settings: is_enabled = output_settings[config_key] + if (("star2" in config_key) and ((system_type == 'CO-HMS') or (system_type == 'CO-HeMS'))): + logger.warning(f"Output setting '{config_key}' is not applicable for system type '{system_type}' and will be ignored.") + is_enabled = False output_layer[section][enabled_param] = to_fortran_bool(is_enabled) # Set filename parameter if provided and feature is enabled @@ -1360,8 +1353,7 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, 'saved_model_name': f"'initial_star1_step{nr_steps-1}.mod'"} - - elif system_type == "HMS-HMS": + elif system_type == "HMS-HMS" or system_type == "CO-HMS": # remove star job and controls for star1 if empty HMS_HMS_keys = [key for key in all_keys if len(POSYDON_inlists.get(key, [])) > 0] #First pass: process file-based layers (MESA, POSYDON, user) @@ -1381,6 +1373,7 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, final_inlists[key].update(posydon_layer_params) final_inlists[key].update(user_layer_params) + all_keys = sorted(final_inlists.keys()) # Clean the final inlist parameters. @@ -1404,7 +1397,7 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, # Build output controls layer if provided if output_settings: - output_layer_dict = _build_output_controls_layer(output_settings) + output_layer_dict = _build_output_controls_layer(output_settings, system_type) for key in all_keys: output_params = output_layer_dict.get(key, {}) @@ -1416,6 +1409,8 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, layer_counts['output'][key] = 0 layer_params['output'][key] = {} + + # Handle ZAMS filenames if provided if output_settings and 'zams_filename_1' in output_settings and output_settings['zams_filename_1'] is not None: final_inlists['binary_star1_controls']['zams_filename'] = f"'{output_settings['zams_filename_1']}'" @@ -1440,6 +1435,7 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, final_inlists['binary_star1_controls'].pop('zams_filename', None) final_inlists['binary_star2_controls'].pop('zams_filename', None) + # Add inlist_names layer for binary systems # This must happen after all other layers to use the constructed run_directory paths if 'single' not in system_type.lower(): From 3afe37bda6bb787aa06d1a71ed470695dfa9d406 Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 6 Feb 2026 10:10:50 +0100 Subject: [PATCH 08/13] add Inlist handling and default MESAInlists loading class --- posydon/CLI/grids/inlist_manipulation.py | 400 +++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 posydon/CLI/grids/inlist_manipulation.py diff --git a/posydon/CLI/grids/inlist_manipulation.py b/posydon/CLI/grids/inlist_manipulation.py new file mode 100644 index 0000000000..d32200b6ba --- /dev/null +++ b/posydon/CLI/grids/inlist_manipulation.py @@ -0,0 +1,400 @@ + +import copy +import os +import re +from abc import ABC, abstractmethod + + +class InlistSection: + """Represents a single namelist section (e.g., &star_job, &controls)""" + def __init__(self, name, parameters=None): + self.name = name + self.parameters = parameters or {} + + def __repr__(self): + """Return a string representation of the section""" + param_count = len(self.parameters) + if param_count == 0: + return f"InlistSection(name='{self.name}', parameters=0)" + + # Show first few parameters as preview + preview_keys = list(self.parameters.keys())[:3] + preview = ", ".join(preview_keys) + if param_count > 3: + preview += ", ..." + + return f"InlistSection(name='{self.name}', parameters={param_count}: [{preview}])" + + def merge(self, other): + """Merge another section into this one (later values override)""" + if self.name != other.name: + raise ValueError("Cannot merge sections with different names") + self.parameters.update(other.parameters) + return self + + def to_string(self): + """Convert the section to a string representation""" + lines = [f"&{self.name}"] + for key, value in self.parameters.items(): + lines.append(f" {key} = {value}") + lines.append("/\n") + return "\n".join(lines) + + def to_fortran(self) -> str: + """Convert to Fortran namelist format""" + lines = [f"&{self.name}"] + for key, value in self.parameters.items(): + lines.append(f"\t{key} = {self._format_value(value)}") + lines.append(f"/ ! end of {self.name} namelist\n") + return "\n".join(lines) + + @staticmethod + def _format_value(value) -> str: + """Return value as-is (no formatting needed)""" + return value + + @staticmethod + def _parse_value(value_str): + """Return parsed value""" + + return value_str.strip() + + @classmethod + def from_string(cls, name: str, content: str): + """Parse a namelist section from string content""" + parameters = {} + + # Split into lines and process + lines = content.split('\n') + for line in lines: + # Remove comments + if '!' in line: + line = line[:line.index('!')] + line = line.strip() + + # Skip empty lines, section headers, and end markers + if not line or line.startswith('&') or line == '/': + continue + + # Parse key = value + if '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + parameters[key] = cls._parse_value(value) + + return cls(name, parameters) + +class Inlist: + """Represents a complete inlist file with multiple sections""" + def __init__(self, name, sections=None): + object.__setattr__(self, 'name', name) + object.__setattr__(self, 'sections', sections if sections is not None else {}) + + def __repr__(self): + """Return a string representation of the inlist""" + section_count = len(self.sections) + if section_count == 0: + return f"Inlist(name='{self.name}', sections=0)" + + section_names = list(self.sections.keys()) + sections_str = ", ".join(section_names) + + return f"Inlist(name='{self.name}', sections={section_count}: [{sections_str}])" + + def __getattr__(self, name): + """Allow attribute-style access to sections (e.g., inlist.controls)""" + # Avoid infinite recursion by checking __dict__ first + if name in ['name', 'sections']: + return object.__getattribute__(self, name) + + # Try to find section by name + sections = object.__getattribute__(self, 'sections') + if name in sections: + return sections[name] + + # If not found, raise AttributeError + raise AttributeError(f"Inlist has no section '{name}'") + + def __setattr__(self, name, value): + """Allow setting sections as attributes""" + # Handle the standard attributes + if name in ['name', 'sections']: + object.__setattr__(self, name, value) + # If value is an InlistSection, add it to sections + elif isinstance(value, InlistSection): + self.sections[name] = value + else: + # For other attributes, use normal behavior + object.__setattr__(self, name, value) + + def add_section(self, section): + """Add or merge a section into the inlist""" + if section.name in self.sections: + self.sections[section.name].merge(section) + else: + self.sections[section.name] = section + + def merge(self, other): + """Merge another inlist into this one (later values override)""" + merged = Inlist(self.name, copy.deepcopy(self.sections)) + for section in other.sections.values(): + merged.add_section(section) + return merged + + def to_file(self, filepath): + """Generate the complete inlist file content""" + lines = [] + + # Add sections + for section in self.sections.values(): + lines.append(section.to_fortran()) + + with open(filepath, 'w') as f: + f.write("\n".join(lines)) + + + @classmethod + def from_file(cls, filepath: str, name: str = None, section: str = None): + """Read and parse an inlist file + + Args: + filepath: Path to the inlist file + name: Optional name for the inlist (defaults to filename) + section: Optional section name if file has no §ion markers (e.g., .defaults files) + + Returns: + Inlist object with parsed sections + """ + import os + + if name is None: + name = os.path.basename(filepath) + + with open(filepath, 'r') as f: + content = f.read() + + return cls.from_string(content, name, section=section) + + @classmethod + def from_string(cls, content: str, name: str = "inlist", section: str = None): + """Parse an inlist from string content + + This follows the same logic as clean_inlist_file for consistency. + + Args: + content: String content of the inlist file + name: Name for the inlist + section: Optional section name if file has no §ion markers (e.g., .defaults files) + + Returns: + Inlist object with parsed sections + """ + inlist = cls(name) + + # Clean inlist into nice list (matching clean_inlist_file logic) + param_value_list = [] + for line in content.split('\n'): + # Strip away all the comments and whitespace + param_and_value = line.strip('\n').strip().split('!')[0].strip() + # Check that this line actually has a parameter and value pair + if param_and_value and ('=' in param_and_value or '&' in param_and_value): + param_value_list.append(param_and_value) + + # Does this inlist have multiple sections? + sections_found = {k: {} for k in param_value_list if '&' in k} + + if not sections_found: + # MESA default files do not have sections, + # because controls and jobs are in separate files + if section: + parameters = {} + for item in param_value_list: + if '=' in item: + key, value = item.split('=', 1) + key = key.strip() + value = value.strip() + # Skip empty values like '', '.' + if value not in ["''", "'.'"]: + parameters[key] = InlistSection._parse_value(value) + + inlist.add_section(InlistSection(section, parameters)) + else: + # Inlist has both job and controls sections marked with & + current_section = None + for item in param_value_list: + if '&' in item: + # New section header + section_name = item.replace('&', '').strip() + current_section = section_name + sections_found[item] = {} + elif '=' in item and current_section: + key, value = item.split('=', 1) + key = key.strip() + value = value.strip() + # Skip empty values like '', '.' + if value not in ["''", "'.'"]: + sections_found['&' + current_section][key] = InlistSection._parse_value(value) + + # Convert to InlistSection objects + for section_key, params in sections_found.items(): + section_name = section_key.replace('&', '').strip() + inlist.add_section(InlistSection(section_name, params)) + + return inlist + + +class MESAInlists(): + """Handles MESA inlists for single and binary star evolution.""" + def __init__(self, path): + self.path = path + # .defaults files don't have §ion markers, so we specify the section name + controls_inlist = Inlist.from_file(f'{self.path}/star/controls.defaults', section='controls') + star_job_inlist = Inlist.from_file(f'{self.path}/star/star_job.defaults', section='star_job') + self.base_star_inlist = controls_inlist.merge(star_job_inlist) + + binary_job_inlist = Inlist.from_file(f'{self.path}/binary/binary_job.defaults', section='binary_job') + binary_controls_inlist = Inlist.from_file(f'{self.path}/binary/binary_controls.defaults', section='binary_controls') + self.base_binary_inlist = binary_job_inlist.merge(binary_controls_inlist) + + # Clean up default parameters (remove read_extra/inlist refs, replace num_x_ctrls placeholders) + sections_to_clean = [ + self.base_star_inlist.controls, + self.base_star_inlist.star_job, + self.base_binary_inlist.binary_controls, + self.base_binary_inlist.binary_job, + ] + for section in sections_to_clean: + section.parameters = self._clean_parameters(section.parameters) + + def _clean_parameters(self, params_dict): + """Clean parameters by removing read_extra/inlist references and replacing num_x_ctrls. + + Parameters + ---------- + params_dict: dict + Dictionary of parameters to clean + + Returns + ------- + dict + Cleaned parameters dictionary + """ + # Remove read_extra and inlist references + cleaned = {k: v for k, v in params_dict.items() + if not any(substring in k for substring in ['read_extra', 'inlist'])} + + # Replace num_x_ctrls with actual index (placeholder in MESA defaults) + keys_to_replace = {k: k.replace('num_x_ctrls', '1') + for k in cleaned.keys() if 'num_x_ctrls' in k} + for old_key, new_key in keys_to_replace.items(): + cleaned[new_key] = cleaned.pop(old_key) + + return cleaned + +class InlistManager: + """Manages multiple inlists for different evolution steps/phases in MESA. Each entry in the lists corresponds to a different step/phase in the evolution sequence.""" + + def __init__(self,): + self.binary_inlists = [] + self.binary_star1_inlists = [] + self.binary_star2_inlists = [] + self.star1_inlists = [] + self.star2_inlists = [] + + + def append_binary_inlist(self, inlist): + self.binary_inlists.append(inlist) + + def append_binary_star1_inlist(self, inlist): + self.binary_star1_inlists.append(inlist) + + def append_binary_star2_inlist(self, inlist): + self.binary_star2_inlists.append(inlist) + + def append_star1_inlist(self, inlist): + self.star1_inlists.append(inlist) + + def append_star2_inlist(self, inlist): + self.star2_inlists.append(inlist) + + def __repr__(self): + return (f"InlistManager(\nbinary_inlists={len(self.binary_inlists)}, \n" + f"binary_star1_inlists={len(self.binary_star1_inlists)}, \n" + f"binary_star2_inlists={len(self.binary_star2_inlists)}, \n" + f"star1_inlists={len(self.star1_inlists)}, \n" + f"star2_inlists={len(self.star2_inlists)})") + + def _write_inlist_group(self, inlists, output_path, single_name, step_name_pattern): + """Helper to write a group of inlists with consistent naming logic. + + Parameters + ---------- + inlists: List of Inlist objects + List of inlists to write + output_path: str + Directory path to write to + single_name: str + Filename if only one inlist (e.g., 'inlist_project') + step_name_pattern: str + Pattern for multiple inlists (e.g., 'inlist_project_step{}') + """ + if not inlists: + return + + os.makedirs(output_path, exist_ok=True) + + if len(inlists) == 1: + inlists[0].to_file(f'{output_path}/{single_name}') + else: + for i, inlist in enumerate(inlists): + inlist.to_file(f'{output_path}/{step_name_pattern.format(i)}') + + def write_inlists(self, output_dir): + """Write all managed inlists to the output directory. + + Parameters + ---------- + output_dir : str + Path to the output directory. + """ + self._write_inlist_group(self.binary_inlists, f'{output_dir}/binary', + 'inlist_project', 'inlist_project_step{}') + self._write_inlist_group(self.binary_star1_inlists, f'{output_dir}/binary', + 'inlist1', 'inlist1_step{}') + self._write_inlist_group(self.binary_star2_inlists, f'{output_dir}/binary', + 'inlist2', 'inlist2_step{}') + self._write_inlist_group(self.star1_inlists, f'{output_dir}/star1', + 'inlist_step0', 'inlist_step{}') + self._write_inlist_group(self.star2_inlists, f'{output_dir}/star2', + 'inlist_step0', 'inlist_step{}') + + + + +# class EvolutionStep: +# """Represents a single evolution step/phase in MESA.""" +# def __init__(self, name, description, inlist): +# self.name = name +# self.description = description +# self.inlist = inlist + + +# class EvolutionSequence: +# """Represents a sequence of evolution steps/phases in MESA.""" +# def __init__(self, base_inlist): +# self.base_inlist = base_inlist +# self.steps = [] # list of EvolutionStep objects + +# def add_step(self, step): +# """Add an evolution step to the sequence""" +# self.steps.append(step) + +# def generate_inlists(self): +# """Generate inlists for each evolution step by merging with base inlist""" +# inlists = {} +# for step in self.steps: +# merged_inlist = self.base_inlist.merge(step.inlist) +# merged_inlist.name = f"{self.base_inlist.name}_{step.name}" +# inlists[step.name] = merged_inlist +# return inlists From 6382e2b1491d8d60ff60bccaaa6ef372269946ef Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 6 Feb 2026 10:12:38 +0100 Subject: [PATCH 09/13] remove setup file --- posydon/CLI/grids/{setup.py => setup_old.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename posydon/CLI/grids/{setup.py => setup_old.py} (100%) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup_old.py similarity index 100% rename from posydon/CLI/grids/setup.py rename to posydon/CLI/grids/setup_old.py From d58622604083e54a1a391e3ea612c939ab6c887e Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 6 Feb 2026 10:13:01 +0100 Subject: [PATCH 10/13] add new setup file; clean slate --- posydon/CLI/grids/setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 posydon/CLI/grids/setup.py diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py new file mode 100644 index 0000000000..e69de29bb2 From 34890a17b0233993bc8a70285fb01525600029fd Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 6 Feb 2026 16:46:58 +0100 Subject: [PATCH 11/13] rework setup to in a cleaner --- posydon/CLI/grids/__init__.py | 2 +- posydon/CLI/grids/inlist_manipulation.py | 57 +- posydon/CLI/grids/setup.py | 916 +++++++++++++++++++++ posydon/CLI/grids/setup_old.py | 8 - posydon/CLI/log.py | 53 ++ posydon/unit_tests/CLI/grids/test_setup.py | 2 +- 6 files changed, 996 insertions(+), 42 deletions(-) create mode 100644 posydon/CLI/log.py diff --git a/posydon/CLI/grids/__init__.py b/posydon/CLI/grids/__init__.py index ae05b9a79b..094d810140 100644 --- a/posydon/CLI/grids/__init__.py +++ b/posydon/CLI/grids/__init__.py @@ -3,6 +3,6 @@ This module provides CLI tools for managing POSYDON MESA grids. """ -from posydon.CLI.grids.setup import run_setup +from posydon.CLI.grids.setup_old import run_setup __all__ = ['run_setup'] diff --git a/posydon/CLI/grids/inlist_manipulation.py b/posydon/CLI/grids/inlist_manipulation.py index d32200b6ba..2176c6fa3e 100644 --- a/posydon/CLI/grids/inlist_manipulation.py +++ b/posydon/CLI/grids/inlist_manipulation.py @@ -51,6 +51,8 @@ def to_fortran(self) -> str: @staticmethod def _format_value(value) -> str: """Return value as-is (no formatting needed)""" + if isinstance(value, bool): + return '.true.' if value else '.false.' return value @staticmethod @@ -147,8 +149,15 @@ def to_file(self, filepath): lines = [] # Add sections - for section in self.sections.values(): - lines.append(section.to_fortran()) + # sort sections controls before star_job and binary_control before binary_job + section_order = ['controls', 'star_job', 'binary_controls', 'binary_job'] + for section_name in section_order: + if section_name in self.sections: + lines.append(self.sections[section_name].to_fortran()) + + for section_name, section in self.sections.items(): + if section_name not in section_order: + lines.append(section.to_fortran()) with open(filepath, 'w') as f: f.write("\n".join(lines)) @@ -292,6 +301,11 @@ def _clean_parameters(self, params_dict): return cleaned + def __repr__(self): + return (f"MESAInlists(path='{self.path}', " + f"base_star_inlist={self.base_star_inlist}, " + f"base_binary_inlist={self.base_binary_inlist})") + class InlistManager: """Manages multiple inlists for different evolution steps/phases in MESA. Each entry in the lists corresponds to a different step/phase in the evolution sequence.""" @@ -302,6 +316,15 @@ def __init__(self,): self.star1_inlists = [] self.star2_inlists = [] + def keys(self): + return ['binary_inlists', 'binary_star1_inlists', 'binary_star2_inlists', 'star1_inlists', 'star2_inlists'] + + def __getitem__(self, key): + if key in self.keys(): + return getattr(self, key) + else: + raise KeyError(f"InlistManager has no key '{key}'") + def append_binary_inlist(self, inlist): self.binary_inlists.append(inlist) @@ -368,33 +391,3 @@ def write_inlists(self, output_dir): 'inlist_step0', 'inlist_step{}') self._write_inlist_group(self.star2_inlists, f'{output_dir}/star2', 'inlist_step0', 'inlist_step{}') - - - - -# class EvolutionStep: -# """Represents a single evolution step/phase in MESA.""" -# def __init__(self, name, description, inlist): -# self.name = name -# self.description = description -# self.inlist = inlist - - -# class EvolutionSequence: -# """Represents a sequence of evolution steps/phases in MESA.""" -# def __init__(self, base_inlist): -# self.base_inlist = base_inlist -# self.steps = [] # list of EvolutionStep objects - -# def add_step(self, step): -# """Add an evolution step to the sequence""" -# self.steps.append(step) - -# def generate_inlists(self): -# """Generate inlists for each evolution step by merging with base inlist""" -# inlists = {} -# for step in self.steps: -# merged_inlist = self.base_inlist.merge(step.inlist) -# merged_inlist.name = f"{self.base_inlist.name}_{step.name}" -# inlists[step.name] = merged_inlist -# return inlists diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index e69de29bb2..93c56b8eaa 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -0,0 +1,916 @@ +'''' +This module provides the setup process for POSYDON MESA grids. + +This function orchestrates the entire setup workflow: +1. Validate environment and inputs +2. Parse configuration file +3. Build configuration stack (MESA → POSYDON → User) +4. Setup run directory with all necessary files +5. Generate submission scripts +''' +import os +import shutil +import subprocess + +from posydon.CLI.grids.inlist_manipulation import ( + Inlist, + InlistManager, + InlistSection, + MESAInlists, +) +from posydon.CLI.log import RESET, logger, setup_logger +from posydon.utils import configfile + +COLUMNS_FILES = {'star_history_columns':'history_columns.list', + 'binary_history_columns':'binary_history_columns.list', + 'profile_columns':'profile_columns.list'} + +EXTRAS_FILES = ['makefile_binary', 'makefile_star', 'binary_run', + 'star_run', 'run_binary_extras', 'run_star_binary_extras', 'run_star1_extras', 'run_star2_extras',] + +def check_file_exist(file_path, raise_error=True): + """Check if a file exists at the given path + + Parameters + ---------- + file_path : str + Path to the file to check + raise_error : bool, optional + If True, raise ValueError when file doesn't exist. + If False, return boolean. Default is True. + + Returns + ------- + bool + True if file exists, False otherwise (only when raise_error=False) + """ + exists = os.path.exists(file_path) + + if not exists: + if raise_error: + print(f"File {file_path} does not exist") + raise ValueError(f"File {file_path} does not exist") + else: + return False + + return True + +def validate_input(args): + + logger.debug("Validating input arguments and environment variables.") + + if 'MESA_DIR' not in os.environ: + raise ValueError( + "MESA_DIR must be defined in your environment " + "before you can run a grid of MESA runs" + ) + + inifile_path = args.inifile + if not os.path.isfile(inifile_path): + raise FileNotFoundError( + "The provided inifile does not exist, please check the path and try again" + ) + logger.debug("Done") + logger.debug('') + + +def find_run_grid_executable(): + """Find the posydon-run-grid executable in the system PATH. + + Returns + ------- + str + Path to the posydon-run-grid executable + + Raises + ------ + ValueError + If the executable cannot be found + """ + proc = subprocess.Popen( + ['which', 'posydon-run-grid'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + (path_to_exec, err) = proc.communicate() + + if not path_to_exec: + raise ValueError('Cannot locate posydon-run-grid executable in your path') + + return path_to_exec.decode('utf-8').strip('\n') + +def read_configuration_file(inifile_path, grid_type): + """Read and parse the configuration file for the grid setup. + + Parameters + ---------- + inifile_path : str + Path to the configuration file + + Returns + ------- + tuple + A tuple containing: + - run_parameters: Dictionary of parameters for running the grid + - slurm: Boolean indicating if SLURM is used for submission + - user_inlist_path: Path to the user's inlist directory + - user_inlist_extras: Additional inlist parameters from the user + """ + logger.debug("Reading configuration file at:") + logger.debug(f"{inifile_path}") + + config_data = configfile.parse_inifile(inifile_path) + # unpack the configuration data into the expected variables + run_parameters = config_data[0] + slurm = config_data[1] + user_inlists = config_data[2] + user_extras = config_data[3] + + # validate the configuration data + if 'keep_profiles' not in run_parameters: + run_parameters['keep_profiles'] = False + + if 'keep_photo' not in run_parameters: + run_parameters['keep_photo'] = False + + # Check if the grid exists + grid_path = run_parameters.get('grid') + if grid_path is None or (not os.path.isfile(grid_path) and not os.path.isdir(grid_path)): + logger.error(grid_path) + raise ValueError( + "Supplied grid does not exist, please check your path and try again" + ) + # Validate dynamic grid requirements + if grid_type == 'dynamic' and 'psycris_inifile' not in run_parameters: + logger.error(run_parameters) + raise ValueError( + "Please add psycris inifile to the [run_parameters] section of the inifile." + ) + + # user_inlists checks + if ('base' not in user_inlists + or user_inlists['base'] is None + or user_inlists['base'] == ''): + logger.error(user_inlists) + raise ValueError( + "Please provide a base ofr the MESA inlists in the configuration file under the [user_inlists] section." + ) + if 'inlist_repository' not in user_inlists: + user_inlists['inlist_repository'] = None + if 'MESA_version' not in user_inlists: + user_inlists['MESA_version'] = 'r11701' + if 'repo_URL' not in user_inlists: + user_inlists['repo_URL'] = 'https://github.com/POSYDON-code/POSYDON-MESA-INLISTS.git' + + + + logger.debug("Configuration file read successfully.") + logger.debug('') + + return run_parameters, slurm, user_inlists, user_extras + +def resolve_files(mesa_files, posydon_files, user_files, file_keys): + """Generic file resolver: priority is USER > POSYDON > MESA + + Parameters + ---------- + mesa_files : dict + posydon_files : dict + user_files : dict + file_keys : list + Keys to resolve + + Returns + ------- + dict + Final resolved files + """ + final_files = {} + for key in file_keys: + if user_files.get(key) is not None: + final_files[key] = user_files[key] + elif posydon_files.get(key) is not None: + final_files[key] = posydon_files[key] + else: + final_files[key] = mesa_files.get(key) + return final_files + + +def setup_posydon_inlist_repository(inlist_repository, + MESA_version, + repo_URL, + ): + logger.info("Setting up POSYDON MESA inlist repository.") + logger.info('This can take a few minutes depending on your internet connection.') + if inlist_repository is None: + # check if inlist_repository is provided + inlist_repository = os.path.expanduser('~') + + # check if the inlist repository path exists + if not os.path.exists(inlist_repository): + os.makedirs(inlist_repository) + + if os.listdir(inlist_repository): + out = subprocess.run(['git', 'remote', '-v'], + cwd=inlist_repository, + check=True, + capture_output=True, + text=True,) + if repo_URL not in out.stdout: + raise ValueError(f"The provided inlist repository path is not empty and does not contain the correct POSYDON inlist repository, please check the path and try again.") + logger.debug('Existing POSYDON inlist repository found.') + logger.debug('Files in folder:') + logger.debug(os.listdir(inlist_repository)) + logger.debug("Files in folder. Assuming the POSYDON inlist repository is already cloned into this folder!") + else: + out = subprocess.run(['git', 'clone', repo_URL, inlist_repository], + capture_output=True, + text=True, + check=True,) + if out.stderr: + logger.error(out.stderr) + else: + logger.info("Cloned the POSYDON inlist repository successfully.") + + try: + out = subprocess.run(['git', 'pull'], + cwd=inlist_repository, + capture_output=True, + text=True, + check=True,) + except subprocess.CalledProcessError as e: + logger.error(f"Error pulling the latest changes: {e.stderr}") + + # check if the base is available as a folder in the repository + version_root_path = os.path.join(inlist_repository, MESA_version) + if not os.path.exists(version_root_path): + logger.error(version_root_path) + raise ValueError("The provided MESA version does not exist in the inlist repository, please check your provided MESA version and try again.") + + logger.info("POSYDON MESA inlist repository setup complete.") + logger.debug("POSYDON MESA inlist repository setup complete:") + logger.debug(version_root_path) + logger.debug('') + return version_root_path + + +def setup_MESA_defaults(path_to_version): + """Setup the MESA default base inlists, extras and columns + + Parameters + ---------- + path_to_version : str + Path to the MESA version in the inlist repository + (root directory of the version) + + Returns + ------- + MESA_default_inlists : dict + Dictionary of MESA default inlists paths + MESA_default_extras : dict + Dictionary of MESA default extras paths + MESA_default_columns : dict + Dictionary of MESA default column files paths + """ + MESA_DIR = os.environ['MESA_DIR'] + logger.debug(f"Setting up MESA defaults from MESA_DIR: {MESA_DIR}") + #---------------------------------- + # Inlists + #---------------------------------- + # Common inlists + # TODO: These are currently stored in the POSYDON inlist repository: + # The default MESA inlists with r11701 has a bug and we needed to fix it. + # We can add this to our changed MESA version, when we release it? + # Then this can be reverted to the MESA default inlists! + path_to_MESA_inlists = os.path.join(path_to_version, 'MESA_defaults', 'inlists') + MESA_inlists = MESAInlists(path_to_MESA_inlists) + + #---------------------------------- + # EXTRAS + #---------------------------------- + + MESA_extras = {} + + # Helper to build MESA work directory paths + def mesa_path(module, *parts): + return os.path.join(MESA_DIR, module, 'work', *parts) + + # Makefiles + MESA_extras['makefile_binary'] = mesa_path('binary', + 'make', + 'makefile') + MESA_extras['makefile_star'] = mesa_path('star', + 'make', + 'makefile') + + # Run files + MESA_extras['star_run'] = mesa_path('star', + 'src', + 'run.f') + MESA_extras['binary_run'] = mesa_path('binary', + 'src', + 'binary_run.f') + + # Extras files for binary evolution + MESA_extras['run_binary_extras'] = mesa_path('binary', + 'src', + 'run_binary_extras.f') + MESA_extras['run_star_binary_extras'] = mesa_path('binary', + 'src', + 'run_star_extras.f') + + # star1_extras and star2_extras are needed for pre-MS formation steps (if any). + # During binary evolution, star_binary_extras is used for both stars. + # #Both stars use the same single-star module extras file + star_extras_path = mesa_path('star', 'src', 'run_star_extras.f') + MESA_extras['run_star1_extras'] = star_extras_path + MESA_extras['run_star2_extras'] = star_extras_path + + # Verify all extras files exist + for _, path in MESA_extras.items(): + check_file_exist(path) + + #---------------------------------- + # Columns + #---------------------------------- + + # Column files from MESA defaults + MESA_columns = { + 'star_history_columns': os.path.join(MESA_DIR, 'star', + 'defaults', 'history_columns.list'), + 'binary_history_columns': os.path.join(MESA_DIR, 'binary', + 'defaults', 'binary_history_columns.list'), + 'profile_columns': os.path.join(MESA_DIR, 'star', + 'defaults', 'profile_columns.list') + } + + # Verify all column files exist + for _, path in MESA_columns.items(): + check_file_exist(path) + + logger.info("MESA defaults setup complete.") + logger.debug('MESA inlists:') + logger.debug(MESA_inlists) + logger.debug('MESA extras:') + logger.debug(MESA_extras) + logger.debug('MESA columns:') + logger.debug(MESA_columns) + logger.debug('') + return MESA_inlists, MESA_extras, MESA_columns + + +def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): + """Setup the POSYDON configuration inlists, extras and columns based on the provided base and system type. + + Parameters + ---------- + path_to_version : str + Path to the POSYDON inlist repository + base : list or str + Base to use for the POSYDON inlists + system_type : str + System type to use for the POSYDON inlists + mesa_inlists : MESAInlists + The MESA default inlists to use as a base for the POSYDON inlists + + Returns + ------- + inlists : dict + Dictionary of POSYDON inlists paths stacked on top of MESA defaults + POSYDON_extras : dict + Dictionary of POSYDON extras paths + POSYDON_columns : dict + Dictionary of POSYDON column files paths + """ + + + if base == "MESA" or base[0] == "MESA": + POSYDON_columns = {name: None for name in COLUMNS_FILES.keys()} + POSYDON_inlists = {} + POSYDON_extras = {key: None for key in EXTRAS_FILES} + return POSYDON_inlists, POSYDON_extras, POSYDON_columns + + if len(base) == 3: + POSYDON_inlist_folder = os.path.join(path_to_version, base[0], base[1], base[2]) + else: + raise ValueError("Base should be a list of 3 elements corresponding to the path in the inlist repository where the POSYDON inlists are located. For example: ['POSYDON', 'DR2', 'dedt_hepulse']") + check_file_exist(POSYDON_inlist_folder) + + logger.debug(f"Setting up POSYDON configuration: {POSYDON_inlist_folder}") + + logger.info("Setting up POSYDON configuration.") + logger.info('Requested POSYDON configuration:') + logger.info(f"Base: {base}") + logger.info(f"System type: {system_type}") + + #---------------------------------- + # Inlists + #---------------------------------- + inlists = InlistManager() + + # Load common base inlists once + base_inlist1 = Inlist.from_file(f'{POSYDON_inlist_folder}/base_inlists/inlist1') + base_inlist2 = Inlist.from_file(f'{POSYDON_inlist_folder}/base_inlists/inlist2') + base_project_inlist = Inlist.from_file(f'{POSYDON_inlist_folder}/base_inlists/inlist_project') + + # Load HeMS setup steps (used by multiple system types) + hems_step_0 = Inlist.from_file(f'{POSYDON_inlist_folder}/HeMS_setup_inlists/inlist_step1') + hems_step_1 = Inlist.from_file(f'{POSYDON_inlist_folder}/HeMS_setup_inlists/inlist_step2') + + # Load single star inlist (used by multiple system types) + single_star_inlist = Inlist.from_file(f'{POSYDON_inlist_folder}/single/single_star_inlist') + + mesa_base_project_inlist = mesa_inlists.base_binary_inlist.merge(base_project_inlist) + mesa_base_star_inlist1 = mesa_inlists.base_star_inlist.merge(base_inlist1) + mesa_base_star_inlist2 = mesa_inlists.base_star_inlist.merge(base_inlist2) + + + if system_type == 'single_HMS': + combined_inlist = mesa_base_star_inlist1.merge(single_star_inlist) + # Add save_model_when_terminate to the inlist for single star runs, since we want to save the final model for the single star case. + parameters = {'save_model_when_terminate': True, + 'save_model_filename': "'initial_star1_step0.mod'"} + combined_inlist.add_section(InlistSection(name='star_job', + parameters=parameters)) + inlists.append_star1_inlist(combined_inlist) + + # TODO: Can we remove the lines below?? + # add specific to "save" the initial model loading in the binary star1 job + parameters = {'create_pre_main_sequence_model': False, + 'load_saved_model': True, + 'saved_model_name': "'initial_star1_step0.mod'"} + tmp_inlist = Inlist(name='binary_star1_inlist') + tmp_inlist.add_section(InlistSection(name='star_job', parameters=parameters)) + inlists.append_binary_star1_inlist(tmp_inlist) + + + elif system_type == 'single_HeMS': + # link each step + parameters = {'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step0.mod'"} + hems_step_0.add_section(InlistSection(name='star_job', parameters=parameters)) + + parameters = {'load_saved_model': True, + 'saved_model_name': "'initial_star1_step0.mod'", + 'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step1.mod'"} + hems_step_1.add_section(InlistSection(name='star_job', parameters=parameters)) + + parameters = {'load_saved_model': True, + 'saved_model_name': "'initial_star1_step1.mod'", + 'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step2.mod'"} + single_star_inlist.add_section(InlistSection(name='star_job', parameters=parameters)) + + inlists.append_star1_inlist(mesa_base_star_inlist1.merge(hems_step_0)) + inlists.append_star1_inlist(mesa_base_star_inlist1.merge(hems_step_1)) + inlists.append_star1_inlist(mesa_base_star_inlist1.merge(single_star_inlist)) + + # TODO: Can we remove the lines below?? + # add specific to "save" the initial model loading in the binary star1 job + parameters = {'create_pre_main_sequence_model': False, + 'load_saved_model': True, + 'saved_model_name': "'initial_star1_step2.mod'"} + tmp_inlist = Inlist(name='binary_star1_inlist') + tmp_inlist.add_section(InlistSection(name='star_job', parameters=parameters)) + inlists.append_binary_star1_inlist(tmp_inlist) + + + elif system_type == 'HMS-HMS': + inlists.append_binary_inlist(mesa_base_project_inlist) + inlists.append_binary_star1_inlist(mesa_base_star_inlist1) + inlists.append_binary_star2_inlist(mesa_base_star_inlist2) + + elif system_type == 'CO-HMS': + co_hms_inlist1 = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HMS/inlist1') + co_hms_inlist2 = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HMS/inlist2') + co_hms_project = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HMS/inlist_project') + + inlists.append_binary_inlist(mesa_base_project_inlist.merge(co_hms_project)) + inlists.append_binary_star1_inlist(mesa_base_star_inlist1.merge(co_hms_inlist1)) + inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(co_hms_inlist2)) + + elif system_type == 'CO-HeMS': + co_inlist1 = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HeMS/binary/inlist1') + co_inlist_project = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HeMS/binary/inlist_project') + + mesa_HMS_base = mesa_base_star_inlist1.merge(base_inlist1) + + inlists.append_binary_inlist(mesa_base_project_inlist.merge(co_inlist_project)) + inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_0)) + inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_1)) + inlists.append_binary_star1_inlist(mesa_HMS_base.merge(base_inlist1)) + inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(base_inlist2).merge(co_inlist1)) + + elif system_type == 'HeMS-HMS': + + mesa_HMS_base = mesa_base_star_inlist1.merge(base_inlist1) + + inlists.append_binary_inlist(mesa_base_project_inlist) + inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_0)) + inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_1)) + inlists.append_binary_star1_inlist(mesa_HMS_base) + inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(base_inlist2)) + + #---------------------------------- + # Extras + #---------------------------------- + + POSYDON_extras = {} + POSYDON_extras['run_binary_extras'] = os.path.join(POSYDON_inlist_folder, + 'extras_files', + 'run_binary_extras.f') + POSYDON_extras['run_star_binary_extras'] = os.path.join(POSYDON_inlist_folder, + 'extras_files', + 'run_star_extras.f') + POSYDON_extras['run_star1_extras'] = os.path.join(POSYDON_inlist_folder, + 'extras_files', + 'run_star_extras.f') + + #---------------------------------- + # Columns + #---------------------------------- + + # Setup POSYDON columns + POSYDON_columns = {} + for name, filename in COLUMNS_FILES.items(): + file = os.path.join(POSYDON_inlist_folder, 'column_files', filename) + # only add if the file exists + if check_file_exist(file, raise_error=False): + POSYDON_columns[name] = file + else: + POSYDON_columns[name] = None + + logger.info("POSYDON configuration setup complete.") + logger.debug('POSYDON inlists:') + logger.debug(inlists) + logger.debug('POSYDON extras:') + logger.debug(POSYDON_extras) + logger.debug('POSYDON columns:') + logger.debug(POSYDON_columns) + logger.debug('') + + return inlists, POSYDON_extras, POSYDON_columns + +def get_additional_user_settings(user_inlists): + """Extract additional settings from configuration for output controls. + + Parameters + ---------- + user_inlists : dict + Dictionary of user inlists and configuration + + Returns + ------- + dict + Dictionary of additional settings + """ + + binary_inlist = Inlist(name='binary_inlists') + binary_star1_inlist = Inlist(name='binary_star1_inlists') + binary_star2_inlist = Inlist(name='binary_star2_inlists') + + + if 'history_interval' in user_inlists: + logger.debug('history_interaval found in user inlists, setting history_interval for binary, binary_star1 and binary_star2 inlists to user value') + interval = user_inlists['history_interval'] + binary_inlist.add_section(InlistSection(name='binary_controls', + parameters={'history_interval': interval})) + binary_star1_inlist.add_section(InlistSection(name='controls', + parameters={'history_interval': interval})) + binary_star2_inlist.add_section(InlistSection(name='controls', + parameters={'history_interval': interval})) + + if 'binary_history' in user_inlists and not user_inlists['binary_history']: + logger.debug('User has set binary_history to False, setting history_interval to -1 to turn off binary history output') + binary_inlist.add_section(InlistSection(name='binary_controls', + parameters={'history_interval': -1})) + + + for star, binary_star_inlist in zip(['star1', 'star2'], + (binary_star1_inlist, binary_star2_inlist)): + + final_profile_key = f'final_profile_{star}' + final_model_key = f'final_model_{star}' + history_key = f'history_{star}' + + if final_profile_key in user_inlists and user_inlists[final_profile_key]: + parameters = {'write_profile_when_terminate': True, + 'filename_for_profile_when_terminate': "'final_profile_star1.data'" if star == 'star1' else "'final_profile_star2.data'"} + + binary_star_inlist.add_section(InlistSection(name=f'star_job', + parameters=parameters)) + + else: + parameters = {'write_profile_when_terminate': False} + binary_star_inlist.add_section(InlistSection(name=f'star_job', + parameters=parameters)) + + if final_model_key in user_inlists: + parameters = {'save_model_when_terminate': True, + 'save_model_filename': "'final_star1.mod'" if star == 'star1' else "'final_star2.mod'"} + + binary_star_inlist.add_section(InlistSection(name=f'star_job', + parameters=parameters)) + + if history_key in user_inlists: + binary_star_inlist.add_section(InlistSection(name=f'controls', + parameters={'do_history_file': user_inlists[history_key]})) + + return (binary_inlist, binary_star1_inlist, binary_star2_inlist) + + +def setup_user(user_inlists, user_extras, POSYDON_inlists): + """Separates out user inlists, extras and columns + + Parameters + ---------- + user_mesa_inlists : dict + Dictionary of user inlists paths from the inifile + user_mesa_extras : dict + Dictionary of user extras paths from the inifile + + + Returns + ------- + user_mesa_inlists : dict + Dictionary of user inlists paths + user_mesa_extras : dict + Dictionary of user extras paths + user_columns : dict + Dictionary of user column files paths + """ + + #---------------------------------- + # Inlists + #---------------------------------- + # separate out inlists from user inlists + logger.debug("Resolving user inlists and merging with POSYDON inlists where applicable.") + + for key in POSYDON_inlists.keys(): + num_steps = len(POSYDON_inlists[key]) + if ('star1_inlists' in key or 'star2_inlists' in key) and not 'binary' in key: + key = key[:-1] # drop the 's' at the end of star1_inlists and star2_inlists to look for user inlist keys + if num_steps == 0: + continue + elif num_steps == 1: + # single step case: look for key without or without step + lookup_key = f"{key}_step0" if not key.endswith("_step0") else key + if lookup_key in user_inlists and user_inlists[lookup_key] is not None: + logger.debug(f"Found user inlist for {key} with key {lookup_key}") + # Add the key to the POSYDON_inlists + star1_user_inlist = Inlist.from_file(user_inlists[lookup_key]) + + elif key in user_inlists and user_inlists[key] is not None: + logger.debug(f"Found user inlist for {key} with key {key}") + # Add the key to the POSYDON_inlists + star1_user_inlist = Inlist.from_file(user_inlists[key]) + else: + continue + POSYDON_inlists[key][0] = POSYDON_inlists[key][0].merge(star1_user_inlist) + + else: + # multiple steps case: look for key_stepX versions + for step in range(num_steps): + lookup_key = f"{key}_step{step}" if not key.endswith(f"_step{step}") else key + if lookup_key in user_inlists and user_inlists[lookup_key] is not None: + logger.debug(f"Found user inlist for {key} step {step} with key {lookup_key}") + star1_user_inlist = Inlist.from_file(user_inlists[lookup_key]) + POSYDON_inlists[key][step] = POSYDON_inlists[key][step].merge(star1_user_inlist) + else: + continue + + else: # binary_inlists and binary_star1/2_inlists + if key in user_inlists and user_inlists[key] is not None: + user_inlist = Inlist.from_file(user_inlists[key]) + POSYDON_inlists[key] = POSYDON_inlists[key].merge(user_inlist) + else: + continue + + logger.info("User inlists resolved and merged where applicable.") + logger.debug('Final inlists after merging with user inlists:') + logger.debug(POSYDON_inlists) + logger.debug('') + + # Additional user inlist parameters that are set in user_inlists, + # but do not correspond to full inlist files. + + logger.info("Checking for additional user inlist parameters in configuration file.") + + additional_user_settings = get_additional_user_settings(user_inlists) + if additional_user_settings: + binary_inlist, binary_star1_inlist, binary_star2_inlist = additional_user_settings + if len(POSYDON_inlists['binary_inlists']) == 0: + POSYDON_inlists['binary_inlists'].append(binary_inlist) + else: + POSYDON_inlists['binary_inlists'][0] = POSYDON_inlists['binary_inlists'][0].merge(binary_inlist) + if len(POSYDON_inlists['binary_star1_inlists']) == 0: + POSYDON_inlists['binary_star1_inlists'].append(binary_star1_inlist) + else: + POSYDON_inlists['binary_star1_inlists'][0] = POSYDON_inlists['binary_star1_inlists'][0].merge(binary_star1_inlist) + if len(POSYDON_inlists['binary_star2_inlists']) == 0: + POSYDON_inlists['binary_star2_inlists'].append(binary_star2_inlist) + else: + POSYDON_inlists['binary_star2_inlists'][0] = POSYDON_inlists['binary_star2_inlists'][0].merge(binary_star2_inlist) + + #---------------------------------- + # Extras + #---------------------------------- + # separate out extras from user inlists + logger.info("Resolving user extras") + + user_extras = {} + for key in EXTRAS_FILES: + if key in user_extras.keys(): + user_extras[key] = user_extras[key] + check_file_exist(user_extras[key]) + else: + user_extras[key] = None + + logger.info("User extras resolved.") + logger.debug('User extras:') + logger.debug(user_extras) + logger.debug('') + + #---------------------------------- + # Columns + #---------------------------------- + # separate out columns from user inlists + logger.info("Resolving user columns") + + user_columns = {} + + # separate out columns from user inlists + for name in COLUMNS_FILES.keys(): + if name in user_inlists.keys(): + user_columns[name] = user_inlists[name] + check_file_exist(user_columns[name]) + else: + user_columns[name] = None + + logger.info("User columns resolved.") + logger.debug('User columns:') + logger.debug(user_columns) + logger.debug('') + + return POSYDON_inlists, user_extras, user_columns + +def _copy_column_files(path, final_columns): + """Copy column list files to the grid run folder. + + Parameters + ---------- + path : str + Path to the grid run folder + final_columns : dict + Dictionary mapping column types to file paths + + Returns + ------- + dict + Dictionary mapping column types to destination paths in the grid run folder + """ + + logger.debug(f'COLUMN LISTS USED:') + out_paths = {} + for key, value in final_columns.items(): + logger.debug(f"{key}: {value}") + dest = os.path.join(path, 'column_lists', COLUMNS_FILES[key]) + shutil.copy(value, dest) + out_paths[key] = dest + return out_paths + +def _copy_extras_files(path, final_extras): + """Copy extras files (makefiles, run files) to the grid run folder. + + Parameters + ---------- + path : str + Path to the grid run folder + final_extras : dict + Dictionary mapping extras keys to file paths + """ + logger.debug(f'EXTRAS USED:') + + # Define destination mapping for each extras key + extras_destinations = { + 'makefile_binary': [('binary/make', 'makefile_binary')], + 'makefile_star': [('star1/make', 'makefile_star'), + ('star2/make', 'makefile_star')], + 'binary_run': [('binary/src', 'binary_run.f')], + 'star_run': [('star1/src', 'run.f'), + ('star2/src', 'run.f')], + 'run_binary_extras': [('binary/src', 'run_binary_extras.f')], + 'run_star_binary_extras': [('binary/src', 'run_star_extras.f')], + 'run_star1_extras': [('star1/src', 'run_star_extras.f')], + 'run_star2_extras': [('star2/src', 'run_star_extras.f')], + } + + for key, value in final_extras.items(): + logger.debug(f"{key}: {value}") + if key == 'mesa_dir': + continue + + if key in extras_destinations: + for subdir, filename in extras_destinations[key]: + dest = os.path.join(path, subdir, filename) + shutil.copy(value, dest) + else: + logger.warning(f"Unrecognized extras key '{key}'. Copying to root.") + shutil.copy(value, path) + + +def create_run_directory(run_directory, inlists, extras, columns): + # Create directory structure + + logger.debug(f"Creating run directory at:") + logger.debug(f'{run_directory}') + + os.makedirs(run_directory, exist_ok=True) + + subdirs = ['binary', 'binary/make', 'binary/src', + 'star1', 'star1/make', 'star1/src', + 'star2', 'star2/make', 'star2/src', + 'column_lists'] + + for subdir in subdirs: + dir_path = os.path.join(run_directory, subdir) + os.makedirs(dir_path, exist_ok=True) + + # columns + _copy_column_files(run_directory, columns) + _copy_extras_files(run_directory, extras) + + inlists.write_inlists(run_directory) + + +def run_setup(args): + """Run the setup process for POSYDON MESA grids. + + + Parameters + ---------- + args : argparse.Namespace + Parsed command-line arguments containing: + - inifile: Path to the configuration file + - grid_type: 'fixed' or 'dynamic' + - run_directory: Output directory + - submission_type: 'shell' or 'slurm' + - nproc: Number of processors + - verbose: Verbosity setting + """ + + setup_logger(args.verbose) + + validate_input(args) + + run_parameters, slurm, user_inlists, user_extras = read_configuration_file(args.inifile, args.grid_type) + + # Setup the run directory + run_directory = args.run_directory + if not os.path.exists(run_directory): + os.makedirs(run_directory) + + # Setup the POSYDON MESA inlist repository + posydon_inlist_repo_path = setup_posydon_inlist_repository( + inlist_repository = user_inlists['inlist_repository'], + MESA_version = user_inlists['MESA_version'], + repo_URL = user_inlists['repo_URL'], + ) + + MESA_inlists, MESA_extras, MESA_columns = setup_MESA_defaults(posydon_inlist_repo_path) + + # Stacks the MESA inlists + (POSYDON_inlists, + POSYDON_extras, + POSYDON_columns) = setup_POSYDON(posydon_inlist_repo_path, + user_inlists['base'], + user_inlists['system_type'], + MESA_inlists) + + user_inlists, user_extras, user_columns = setup_user(user_inlists, user_extras, POSYDON_inlists) + + # add grid parameters from run_parameters to the inlist stack. + + final_extras = resolve_files(MESA_extras, POSYDON_extras, user_extras, EXTRAS_FILES) + final_columns = resolve_files(MESA_columns, POSYDON_columns, user_columns, COLUMNS_FILES.keys()) + + # create the run directory with the final inlists, extras and columns + create_run_directory(run_directory, POSYDON_inlists, final_extras, final_columns) + + + # write/copy extras + # write/copy columns + # write/copy the makefiles + + # write/copy inlists + # Build the configuration stack + write the run directory + # 1. Get the POSYDON MESA inlist repository + # 2. Get the MESA defaults inlists + # 3. Check the config for POSYDON base + # 4. Add the user inlist base to the stack + # write/copy the submission scripts + + + path_to_run_grid_exe = find_run_grid_executable() diff --git a/posydon/CLI/grids/setup_old.py b/posydon/CLI/grids/setup_old.py index b2bf66b4b2..d6d22461cf 100644 --- a/posydon/CLI/grids/setup_old.py +++ b/posydon/CLI/grids/setup_old.py @@ -1448,14 +1448,6 @@ def resolve_inlists(MESA_default_inlists, POSYDON_inlists, } final_inlists['binary_job'].update(inlist_names_params) - # Track this layer - for key in all_keys: - if key == 'binary_job': - layer_counts['inlist_names'][key] = len(inlist_names_params) - layer_params['inlist_names'][key] = inlist_names_params - else: - layer_counts['inlist_names'][key] = 0 - layer_params['inlist_names'][key] = {} else: # Single star systems don't need inlist_names for key in all_keys: diff --git a/posydon/CLI/log.py b/posydon/CLI/log.py new file mode 100644 index 0000000000..da13b2c69c --- /dev/null +++ b/posydon/CLI/log.py @@ -0,0 +1,53 @@ + +import logging + +# ANSI color codes +GREEN = '\033[92m' +GRAY = '\033[90m' +CYAN = '\033[96m' +YELLOW = '\033[93m' +MAGENTA = '\033[95m' +RED = '\033[91m' +RESET = '\033[0m' +BOLD = '\033[1m' + +# Setup logger +logger = logging.getLogger(__name__) + +class ColoredFormatter(logging.Formatter): + """Custom formatter that adds colors to log level names.""" + + LEVEL_COLORS = { + 'DEBUG': GRAY, + 'INFO': CYAN, + 'WARNING': YELLOW, + 'ERROR': RED, + 'CRITICAL': RED + } + + def format(self, record): + # Get the color for this log level + color = self.LEVEL_COLORS.get(record.levelname, RESET) + + # Create the formatted message with colored level name + formatted = f'[{color}{record.levelname}{RESET}] {record.getMessage()}' + return formatted + +def setup_logger(verbose=False): + """Setup logging configuration based on verbosity level. + + Parameters + ---------- + verbose : bool, optional + If False, set INFO level (default) + If True, set DEBUG level for detailed output + """ + level = logging.DEBUG if verbose else logging.INFO + handler = logging.StreamHandler() + handler.setFormatter(ColoredFormatter()) + + logging.basicConfig( + level=level, + handlers=[handler], + force=True + ) diff --git a/posydon/unit_tests/CLI/grids/test_setup.py b/posydon/unit_tests/CLI/grids/test_setup.py index e921d5970e..a21011b141 100644 --- a/posydon/unit_tests/CLI/grids/test_setup.py +++ b/posydon/unit_tests/CLI/grids/test_setup.py @@ -11,7 +11,7 @@ import pytest # import the module which will be tested -import posydon.CLI.grids.setup as totest +import posydon.CLI.grids.setup_old as totest class TestSetupMESADefaults: From 6f8e9f0f2652bcae7a70e4ef923ef4f22eef8aaa Mon Sep 17 00:00:00 2001 From: Max Briel Date: Fri, 6 Feb 2026 22:58:58 +0100 Subject: [PATCH 12/13] add more selections --- posydon/CLI/grids/setup.py | 161 ++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 13 deletions(-) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index 93c56b8eaa..c9ac60e956 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -12,6 +12,8 @@ import shutil import subprocess +import pandas as pd + from posydon.CLI.grids.inlist_manipulation import ( Inlist, InlistManager, @@ -19,6 +21,7 @@ MESAInlists, ) from posydon.CLI.log import RESET, logger, setup_logger +from posydon.grids.psygrid import PSyGrid from posydon.utils import configfile COLUMNS_FILES = {'star_history_columns':'history_columns.list', @@ -100,6 +103,49 @@ def find_run_grid_executable(): return path_to_exec.decode('utf-8').strip('\n') +def read_grid_file(filepath): + """Read grid file and return grid parameters as a dictionary. + + Parameters + ---------- + filepath : str + Path to the grid file + + Returns + ------- + number of grid points : int + Number of grid points in the grid + grid parameter names : set + Set of grid parameter names (column names) + fixgrid_file_name : str + Path to the fixed grid file used for the grid + """ + # TODO: I"m not sure what the last option is for. + # Is it processing runs for a grid stored in a directory? + if '.csv' in filepath: + grid_df = pd.read_csv(filepath) + fixgrid_file_name = filepath + elif '.h5' in filepath: + psy_grid = PSyGrid() + psy_grid.load(filepath) + grid_df = psy_grid.get_pandas_initial_final() + psy_grid.close() + fixgrid_file_name = filepath + elif os.path.isdir(filepath): + PSyGrid().create(filepath, "./fixed_grid_results.h5", slim=True) + psy_grid = PSyGrid() + psy_grid.load("./fixed_grid_results.h5") + grid_df = psy_grid.get_pandas_initial_final() + psy_grid.close() + fixgrid_file_name = os.path.join(os.getcwd(), "fixed_grid_results.h5") + else: + raise ValueError('Grid format not recognized, please feed in an acceptable format: csv') + + grid_parameters = set(grid_df.columns.tolist()) + grid_parameters = [params.lower() for params in grid_parameters] + + return (len(grid_df), grid_parameters, fixgrid_file_name) + def read_configuration_file(inifile_path, grid_type): """Read and parse the configuration file for the grid setup. @@ -360,7 +406,7 @@ def mesa_path(module, *parts): return MESA_inlists, MESA_extras, MESA_columns -def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): +def setup_POSYDON(path_to_version, base, system_type, mesa_inlists, run_directory, ZAMS_filenames): """Setup the POSYDON configuration inlists, extras and columns based on the provided base and system type. Parameters @@ -373,7 +419,10 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): System type to use for the POSYDON inlists mesa_inlists : MESAInlists The MESA default inlists to use as a base for the POSYDON inlists - + run_directory : str + Directory where the run will be executed + ZAMS_filenames : list or str + Filename for the ZAMS model to be used Returns ------- inlists : dict @@ -383,8 +432,6 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): POSYDON_columns : dict Dictionary of POSYDON column files paths """ - - if base == "MESA" or base[0] == "MESA": POSYDON_columns = {name: None for name in COLUMNS_FILES.keys()} POSYDON_inlists = {} @@ -404,6 +451,15 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): logger.info(f"Base: {base}") logger.info(f"System type: {system_type}") + if len(ZAMS_filenames) == 2: + ZAMS_filename_1 = ZAMS_filenames[0] + ZAMS_filename_2 = ZAMS_filenames[1] + elif len(ZAMS_filenames) == 1: + ZAMS_filename_1 = ZAMS_filenames[0] + ZAMS_filename_2 = ZAMS_filenames[0] + else: + raise ValueError("ZAMS_filenames not understood") + #---------------------------------- # Inlists #---------------------------------- @@ -433,6 +489,8 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): 'save_model_filename': "'initial_star1_step0.mod'"} combined_inlist.add_section(InlistSection(name='star_job', parameters=parameters)) + combined_inlist.add_section(InlistSection(name='controls', + parameters={'zams_filename': f"'{ZAMS_filename_1}'"})) inlists.append_star1_inlist(combined_inlist) # TODO: Can we remove the lines below?? @@ -478,9 +536,17 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): elif system_type == 'HMS-HMS': + inlists.append_binary_inlist(mesa_base_project_inlist) - inlists.append_binary_star1_inlist(mesa_base_star_inlist1) - inlists.append_binary_star2_inlist(mesa_base_star_inlist2) + + tmp_inlist = Inlist(name='binary_star1_inlist') + tmp_inlist.add_section(InlistSection(name='controls', parameters={'zams_filename': f"'{ZAMS_filename_1}'"})) + inlists.append_binary_star1_inlist(mesa_base_star_inlist1.merge(tmp_inlist)) + + tmp_inlist = Inlist(name='binary_star2_inlist') + tmp_inlist.add_section(InlistSection(name='controls', parameters={'zams_filename': f"'{ZAMS_filename_2}'"})) + inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(tmp_inlist)) + elif system_type == 'CO-HMS': co_hms_inlist1 = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HMS/inlist1') @@ -488,6 +554,11 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): co_hms_project = Inlist.from_file(f'{POSYDON_inlist_folder}/CO-HMS/inlist_project') inlists.append_binary_inlist(mesa_base_project_inlist.merge(co_hms_project)) + + co_hms_inlist1.add_section(InlistSection(name='controls', parameters={'zams_filename': f"'{ZAMS_filename_1}'"})) + # TODO: we don't really need the secondary.... + co_hms_inlist2.add_section(InlistSection(name='controls', parameters={'zams_filename': f"'{ZAMS_filename_2}'"})) + inlists.append_binary_star1_inlist(mesa_base_star_inlist1.merge(co_hms_inlist1)) inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(co_hms_inlist2)) @@ -511,7 +582,27 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists): inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_0)) inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_1)) inlists.append_binary_star1_inlist(mesa_HMS_base) - inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(base_inlist2)) + + tmp_inlist = Inlist(name='binary_star2_inlist') + tmp_inlist.add_section(InlistSection(name='controls', parameters={'zams_filename': f"'{ZAMS_filename_2}'"})) + + inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(base_inlist2).merge(tmp_inlist)) + + + else: + raise ValueError(f"System type {system_type} not recognized. Please check your configuration file and try again.") + + if not 'single' in system_type: + inlist_star1_binary = os.path.join(run_directory, 'binary', 'inlist1') + inlist_star2_binary = os.path.join(run_directory, 'binary', 'inlist2') + + inlist_names_params = { + 'inlist_names(1)': f"'{inlist_star1_binary}'", + 'inlist_names(2)': f"'{inlist_star2_binary}'" + } + tmp_inlist = Inlist(name='binary_inlist') + tmp_inlist.add_section(InlistSection(name='binary_job', parameters=inlist_names_params)) + inlists.binary_inlists[0] = inlists.binary_inlists[0].merge(tmp_inlist) #---------------------------------- # Extras @@ -714,6 +805,7 @@ def setup_user(user_inlists, user_extras, POSYDON_inlists): else: POSYDON_inlists['binary_star2_inlists'][0] = POSYDON_inlists['binary_star2_inlists'][0].merge(binary_star2_inlist) + #---------------------------------- # Extras #---------------------------------- @@ -844,6 +936,35 @@ def create_run_directory(run_directory, inlists, extras, columns): inlists.write_inlists(run_directory) +def add_grid_parameters_to_inlists(inlists, grid_parameters): + + nr, parameters, fixgrid_filename = read_grid_file(grid_parameters['grid']) + print(parameters) + + # TODO: the old code added the extra read to the star1/star2 inlists. + # for key in inlists.keys(): + # if not 'star1' in key and not 'star2' in key: + # continue + # inlist_list = inlists[key] + # for i in range(len(inlist_list)): + # inlist = inlist_list[i] + # for section in inlist.sections.keys(): + # for param in parameters: + # if param in inlist.sections[section].parameters: + # logger.debug(f'\t\t{param} is in {section} section of {inlist.name} inlist') + # tmp_key = 'star1' if 'star1' in key else 'star2' + # binary_key = 'binary_' if 'binary' in key else '' + # out_params = {f'read_extra_{section}_inlist1': True, + # f'extra_{section}_inlist1_name': f"'inlist_grid_{tmp_key}_{binary_key}{section}'"} + # # add an read_extras to the inlist with the grid parameters + # tmp_inlist = Inlist(name=f'{key}') + # tmp_inlist.add_section(InlistSection(name=section, + # parameters=out_params)) + + # inlist_list[i] = inlist_list[i].merge(tmp_inlist) + + return inlists + def run_setup(args): """Run the setup process for POSYDON MESA grids. @@ -881,29 +1002,43 @@ def run_setup(args): MESA_inlists, MESA_extras, MESA_columns = setup_MESA_defaults(posydon_inlist_repo_path) + print(user_inlists['zams_filename']) + if 'zams_filename' in user_inlists: + ZAMS_filenames = (user_inlists['zams_filename'], + user_inlists['zams_filename']) + elif 'zams_filename_1' in user_inlists and 'zams_filename_2' in user_inlists: + ZAMS_filenames = (user_inlists['zams_filename_1'], + user_inlists['zams_filename_2']) + else: + ZAMS_filenames = (None, None) + + # Stacks the MESA inlists (POSYDON_inlists, POSYDON_extras, POSYDON_columns) = setup_POSYDON(posydon_inlist_repo_path, user_inlists['base'], user_inlists['system_type'], - MESA_inlists) + MESA_inlists, + run_directory, + ZAMS_filenames) user_inlists, user_extras, user_columns = setup_user(user_inlists, user_extras, POSYDON_inlists) # add grid parameters from run_parameters to the inlist stack. + # write/copy extras final_extras = resolve_files(MESA_extras, POSYDON_extras, user_extras, EXTRAS_FILES) + + # write/copy columns final_columns = resolve_files(MESA_columns, POSYDON_columns, user_columns, COLUMNS_FILES.keys()) + # Add grid_parameters to the POSYDON_inlists if needed, by creating a new inlist with the grid parameters and merging it on top of the existing stack. + POSYDON_inlists = add_grid_parameters_to_inlists(POSYDON_inlists, run_parameters) + # create the run directory with the final inlists, extras and columns create_run_directory(run_directory, POSYDON_inlists, final_extras, final_columns) - - # write/copy extras - # write/copy columns - # write/copy the makefiles - # write/copy inlists # Build the configuration stack + write the run directory # 1. Get the POSYDON MESA inlist repository From 69440d083a3abe826c776af32d740e71a123e353 Mon Sep 17 00:00:00 2001 From: Max Briel Date: Sat, 7 Feb 2026 10:12:20 +0100 Subject: [PATCH 13/13] add linking other He grids + add grid_parameters read in for binary grids --- posydon/CLI/grids/setup.py | 118 ++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 40 deletions(-) diff --git a/posydon/CLI/grids/setup.py b/posydon/CLI/grids/setup.py index c9ac60e956..6e944ef39d 100644 --- a/posydon/CLI/grids/setup.py +++ b/posydon/CLI/grids/setup.py @@ -481,6 +481,11 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists, run_director mesa_base_star_inlist1 = mesa_inlists.base_star_inlist.merge(base_inlist1) mesa_base_star_inlist2 = mesa_inlists.base_star_inlist.merge(base_inlist2) + # remove the zams_filename from the base inlists since we will add it in the POSYDON inlists based on the configuration, and we want to avoid confusion about which zams model is being used. + mesa_base_star_inlist1.controls.parameters.pop('zams_filename') + mesa_base_star_inlist2.controls.parameters.pop('zams_filename') + hems_step_0.controls.parameters.pop('zams_filename', None) # not all steps have zams_filename, so use pop with default value to avoid errors + hems_step_1.controls.parameters.pop('zams_filename', None) if system_type == 'single_HMS': combined_inlist = mesa_base_star_inlist1.merge(single_star_inlist) @@ -569,16 +574,43 @@ def setup_POSYDON(path_to_version, base, system_type, mesa_inlists, run_director mesa_HMS_base = mesa_base_star_inlist1.merge(base_inlist1) inlists.append_binary_inlist(mesa_base_project_inlist.merge(co_inlist_project)) + + parameters = {'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step0.mod'"} + + hems_step_0.add_section(InlistSection(name='star_job', parameters=parameters)) inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_0)) + + parameters = {'load_saved_model': True, + 'saved_model_name': "'initial_star1_step0.mod'", + 'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step1.mod'"} + + hems_step_1.add_section(InlistSection(name='star_job', parameters=parameters)) inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_1)) - inlists.append_binary_star1_inlist(mesa_HMS_base.merge(base_inlist1)) - inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(base_inlist2).merge(co_inlist1)) + + parameters = {'load_saved_model': True, + 'saved_model_name': "'initial_star1_step1.mod'"} + + co_inlist1.add_section(InlistSection(name='star_job', parameters=parameters)) + inlists.append_binary_star1_inlist(mesa_HMS_base.merge(base_inlist1).merge(co_inlist1)) + + inlists.append_binary_star2_inlist(mesa_base_star_inlist2.merge(base_inlist2)) elif system_type == 'HeMS-HMS': mesa_HMS_base = mesa_base_star_inlist1.merge(base_inlist1) - inlists.append_binary_inlist(mesa_base_project_inlist) + + parameters = {'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step0.mod'"} + hems_step_0.add_section(InlistSection(name='star_job', parameters=parameters)) + parameters = {'load_saved_model': True, + 'saved_model_name': "'initial_star1_step0.mod'", + 'save_model_when_terminate': True, + 'save_model_filename': f"'initial_star1_step1.mod'"} + hems_step_1.add_section(InlistSection(name='star_job', parameters=parameters)) + inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_0)) inlists.append_star1_inlist(mesa_HMS_base.merge(hems_step_1)) inlists.append_binary_star1_inlist(mesa_HMS_base) @@ -940,28 +972,30 @@ def add_grid_parameters_to_inlists(inlists, grid_parameters): nr, parameters, fixgrid_filename = read_grid_file(grid_parameters['grid']) print(parameters) - - # TODO: the old code added the extra read to the star1/star2 inlists. - # for key in inlists.keys(): - # if not 'star1' in key and not 'star2' in key: - # continue - # inlist_list = inlists[key] - # for i in range(len(inlist_list)): - # inlist = inlist_list[i] - # for section in inlist.sections.keys(): - # for param in parameters: - # if param in inlist.sections[section].parameters: - # logger.debug(f'\t\t{param} is in {section} section of {inlist.name} inlist') - # tmp_key = 'star1' if 'star1' in key else 'star2' - # binary_key = 'binary_' if 'binary' in key else '' - # out_params = {f'read_extra_{section}_inlist1': True, - # f'extra_{section}_inlist1_name': f"'inlist_grid_{tmp_key}_{binary_key}{section}'"} - # # add an read_extras to the inlist with the grid parameters - # tmp_inlist = Inlist(name=f'{key}') - # tmp_inlist.add_section(InlistSection(name=section, - # parameters=out_params)) - - # inlist_list[i] = inlist_list[i].merge(tmp_inlist) + # TODO: the old code added the extra read to the binary_star1/star2 inlists. + # I think it should be added to star1/star2 inlists too for the single star case. + for key in inlists.keys(): + print(key) + if not 'binary_star1' in key and not 'binary_star2' in key: + continue + print('here', key) + inlist_list = inlists[key] + for i in range(len(inlist_list)): + inlist = inlist_list[i] + for section in inlist.sections.keys(): + for param in parameters: + if param in inlist.sections[section].parameters: + logger.debug(f'\t\t{param} is in {section} section of {inlist.name} inlist') + tmp_key = 'star1' if 'star1' in key else 'star2' + binary_key = 'binary_' if 'binary' in key else '' + out_params = {f'read_extra_{section}_inlist1': True, + f'extra_{section}_inlist1_name': f"'inlist_grid_{tmp_key}_{binary_key}{section}'"} + # add an read_extras to the inlist with the grid parameters + tmp_inlist = Inlist(name=f'{key}') + tmp_inlist.add_section(InlistSection(name=section, + parameters=out_params)) + + inlist_list[i] = inlist_list[i].merge(tmp_inlist) return inlists @@ -986,7 +1020,7 @@ def run_setup(args): validate_input(args) - run_parameters, slurm, user_inlists, user_extras = read_configuration_file(args.inifile, args.grid_type) + run_parameters, slurm, user_inlists_params, user_extras = read_configuration_file(args.inifile, args.grid_type) # Setup the run directory run_directory = args.run_directory @@ -995,20 +1029,20 @@ def run_setup(args): # Setup the POSYDON MESA inlist repository posydon_inlist_repo_path = setup_posydon_inlist_repository( - inlist_repository = user_inlists['inlist_repository'], - MESA_version = user_inlists['MESA_version'], - repo_URL = user_inlists['repo_URL'], + inlist_repository = user_inlists_params['inlist_repository'], + MESA_version = user_inlists_params['MESA_version'], + repo_URL = user_inlists_params['repo_URL'], ) MESA_inlists, MESA_extras, MESA_columns = setup_MESA_defaults(posydon_inlist_repo_path) - print(user_inlists['zams_filename']) - if 'zams_filename' in user_inlists: - ZAMS_filenames = (user_inlists['zams_filename'], - user_inlists['zams_filename']) - elif 'zams_filename_1' in user_inlists and 'zams_filename_2' in user_inlists: - ZAMS_filenames = (user_inlists['zams_filename_1'], - user_inlists['zams_filename_2']) + print(user_inlists_params['zams_filename']) + if 'zams_filename' in user_inlists_params: + ZAMS_filenames = (user_inlists_params['zams_filename'], + user_inlists_params['zams_filename']) + elif 'zams_filename_1' in user_inlists_params and 'zams_filename_2' in user_inlists_params: + ZAMS_filenames = (user_inlists_params['zams_filename_1'], + user_inlists_params['zams_filename_2']) else: ZAMS_filenames = (None, None) @@ -1017,13 +1051,13 @@ def run_setup(args): (POSYDON_inlists, POSYDON_extras, POSYDON_columns) = setup_POSYDON(posydon_inlist_repo_path, - user_inlists['base'], - user_inlists['system_type'], + user_inlists_params['base'], + user_inlists_params['system_type'], MESA_inlists, run_directory, ZAMS_filenames) - user_inlists, user_extras, user_columns = setup_user(user_inlists, user_extras, POSYDON_inlists) + user_inlists, user_extras, user_columns = setup_user(user_inlists_params, user_extras, POSYDON_inlists) # add grid parameters from run_parameters to the inlist stack. @@ -1034,7 +1068,11 @@ def run_setup(args): final_columns = resolve_files(MESA_columns, POSYDON_columns, user_columns, COLUMNS_FILES.keys()) # Add grid_parameters to the POSYDON_inlists if needed, by creating a new inlist with the grid parameters and merging it on top of the existing stack. - POSYDON_inlists = add_grid_parameters_to_inlists(POSYDON_inlists, run_parameters) + # TODO: remove if statement? This is to mimick the old code, which doesn't add + # the grid parameter to the single star inlist, but adds it to binary stars. + # I'm not sure if this is intentional or an oversight, but it seems more consistent to add the grid parameters to the single star inlist as well, since it can be used for both single and binary star runs. + if not 'single' in user_inlists_params['system_type']: + POSYDON_inlists = add_grid_parameters_to_inlists(POSYDON_inlists, run_parameters) # create the run directory with the final inlists, extras and columns create_run_directory(run_directory, POSYDON_inlists, final_extras, final_columns)