"""
Constants and configuration management module for GUIBRUSHR.
This module handles all constant variables, configuration loading, and UI-related constants
used throughout the GUIBRUSHR application. It provides centralized access to configuration
parameters, paths, and UI settings.
"""
import os
import warnings
from pathlib import Path
from typing import Dict, List, Any, Union
import yaml
from pandas import read_csv
from screeninfo import get_monitors
import matplotlib.colors as mcolors
def _load_yaml_config(file_path: str) -> Dict[str, Any]:
"""Load and parse YAML configuration file."""
try:
with open(file_path, 'r') as f:
return yaml.safe_load(f)
except Exception as e:
raise RuntimeError(f"Failed to load YAML configuration {file_path}: {str(e)}")
def _load_configuration(path_default) -> tuple[Path, List[str]]:
"""Load configuration paths from CSV file."""
if os.environ.get('SPHINX_BUILD'):
return Path('/dummy/prt/path'), ['/dummy/path1', '/dummy/path2']
try:
df = read_csv(str(Path(path_default, "GUIBRUSHR", "Files",
"Configuration_Path", "configuration.csv")))
paths = df["Path"]
if not Path(str(paths[0])).exists():
warnings.warn(f"PRT path does not exist: {paths[0]}. Please check the configuration.", RuntimeWarning)
exit()
if not Path(str(paths[1])).exists():
warnings.warn(f"Target path does not exist: {paths[1]}. Please check the configuration.", RuntimeWarning)
exit()
return Path(str(df["Path"][0])), df["Path"].tolist()
except Exception as e:
raise RuntimeError(f"Failed to load configuration paths: {str(e)}")
def _get_screen_dimensions() -> tuple[int, int]:
"""Get screen dimensions with fallback values."""
if os.environ.get('SPHINX_BUILD'):
return 2200, 1000
try:
monitor = get_monitors()[0]
return monitor.width, monitor.height
except Exception as e:
print(f"Warning: Using default screen dimensions due to: {e}")
return 2200, 1000
def _load_molecular_configs(path_petitradtrans, PARAMS_LIST, LIST_ELEMENT_FOR_HYBRID, condensed_yaml) -> tuple[List[str], List[str], int, int, int, Dict[str, Dict], Dict[str, str], List[str], Dict[str, str]]:
"""Load molecular and condensed species configurations and update parameter lists.
Args:
path_petitradtrans: Path to petitRADTRANS installation
PARAMS_LIST: List of existing parameters
LIST_ELEMENT_FOR_HYBRID: List of elements for hybrid calculations
condensed_yaml: Condensed species configurations
Returns:
tuple containing:
- List of molecular species
- Updated parameter list
- Start index for molecular species
- Start index for elements
- Start index for condensed
- Dictionary of condensed species
- Dictionary mapping condensed species names to keys
- List of condensed species names
- Dictionary of condensed species labels
"""
if os.environ.get('SPHINX_BUILD'):
return [], PARAMS_LIST, 0, 0, 0, {}, {}, [], {}
try:
# Load molecular configurations
paths = Path(path_petitradtrans, "opacities", "lines", "line_by_line")
molecs = sorted([d.name for d in paths.iterdir() if d.is_dir()])
# Load condensed species configurations
all_condensed_molecs_dict = condensed_yaml
all_condensed_molec = {val["name"]: key for key, val in all_condensed_molecs_dict.items()}
paths_condensed = Path(path_petitradtrans, "opacities", "continuum", "clouds")
condensed_names = [cloud.name.removesuffix(".cotable.petitRADTRANS.h5")
for cloud in sorted(paths_condensed.glob('**/*.h5'))]
dict_labels_condensed_molec = {key: val["label"]
for key, val in all_condensed_molecs_dict.items()}
start_molecs = len(PARAMS_LIST)
params_list = PARAMS_LIST + molecs
start_elements = len(params_list)
params_list += LIST_ELEMENT_FOR_HYBRID
start_condensed_molecs = len(params_list)
params_list += all_condensed_molec
# print(f"MOLECULES: {molecs}")
# print(f"HYBRID ELEMENTS: {LIST_ELEMENT_FOR_HYBRID}")
# print(f"CONDENSED: {all_condensed_molec}")
# print(f"PARAMETERS LIST: {params_list}")
return (molecs, params_list, start_molecs, start_elements, start_condensed_molecs,
all_condensed_molecs_dict, all_condensed_molec,
condensed_names, dict_labels_condensed_molec)
except Exception as e:
raise RuntimeError(f"Failed to load molecular configurations: {str(e)}")
[docs]
class ConstantVariables:
"""
Central configuration and constants management class.
This class handles loading and managing all constant variables, configuration parameters,
and UI settings used throughout the application. It provides a single source of truth
for all constant values and configuration parameters.
"""
# Base paths and configuration loading
path_default: Path = Path(__file__).parent.parent.parent.parent.resolve()
ncore = os.cpu_count() - 3 # Reserve 3 cores: 1 for the GUI + 2 for retrieval plots
# Load configurations
path_petitradtrans, paths = _load_configuration(path_default)
configuration_yaml_path = str(Path(path_default, "GUIBRUSHR", "Files", "Configuration_Yaml"))
parameters_yaml = _load_yaml_config(str(Path(configuration_yaml_path, "parameters.yaml")))
molecules_yaml = _load_yaml_config(str(Path(configuration_yaml_path, "molecules.yaml")))
hybrid_yaml = _load_yaml_config(str(Path(configuration_yaml_path, "hybrid_elements.yaml")))
general_yaml = _load_yaml_config(str(Path(configuration_yaml_path, "general.yaml")))
condensed_yaml = _load_yaml_config(str(Path(configuration_yaml_path, "condensed.yaml")))
personalization_file = _load_yaml_config(str(Path(path_default, "GUIBRUSHR", "General_Constants/personalization.yaml")))
# UI Configuration
WINDOW_WIDTH: int = 800
WINDOW_HEIGHT: int = 600
screen_width, screen_height = _get_screen_dimensions()
position_x = (screen_width - WINDOW_WIDTH) // 2
position_y = (screen_height - WINDOW_HEIGHT) // 2
geometry = f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}+{position_x}+{position_y}"
# Database and Table Configuration
COLUMN_WITH_MULTIPLE_VALUES: List[str] = ["instruments"]
NEW_COLUMN_RETRIEVAL_DB: List[str] = ["instruments"]
NEW_COLUMN_RETRIEVAL_DB_TYPE: List[str] = ["character varying(200)"]
# Application Constants
HEIGHT_TABLE_FILTER: int = 8
# Chemical Species Configuration
NOT_VMR_METALS: str = general_yaml["not_vmr_metals"]
HCNO_NEUTRALS: str = general_yaml['HCNO_neutrals']
IONS: str = general_yaml['ions']
ALKALI: str = general_yaml['alkali']
METALS: str = general_yaml['metals']
METAL_OXIDES: str = general_yaml['metal_oxides']
LIST_ELEMENT_FOR_HYBRID: str = sorted(general_yaml["list_element_for_hybrid"].split())
CLIGHT = float(general_yaml["clight"])
SOLAR_TO_JUPITER_MASSES = float(general_yaml["solar_to_jupiter_masses"])
AU = float(general_yaml["au"])
R_JUP_MEAN = float(general_yaml["r_jup_mean"])
R_JUP = float(general_yaml["r_jup"])
M_JUP = float(general_yaml["m_jup"])
R_SUN = float(general_yaml["r_sun"])
M_SUN = float(general_yaml["m_sun"])
G = float(general_yaml["G"])
RATIO_RSUN_RJUP = R_SUN / R_JUP
RATIO_RSUN_RJUP_MEAN = R_SUN / R_JUP_MEAN
# Molecular and Element Configuration
ALL_MOLECS_DICT: Dict[str, Dict] = molecules_yaml
ALL_MOLEC: List[str] = list(ALL_MOLECS_DICT.keys())
DICT_LABELS_MOLEC: Dict[str, str] = {key: val["label"] for key, val in ALL_MOLECS_DICT.items()}
ELEMENTS_DICT: Dict[str, Dict] = hybrid_yaml
ALL_ELEMENT: List[str] = list(ELEMENTS_DICT.keys())
DICT_LABELS_ELEMENTS: Dict[str, str] = {key: val["label"] for key, val in ELEMENTS_DICT.items()}
# Parameters Configuration
PARAMS_DICT: Dict[str, Dict] = parameters_yaml
PARAMS_LIST: List[str] = list(PARAMS_DICT.keys())
LIST_MULTIPLE_PARAM: List[str] = [key for key, details in PARAMS_DICT.items() if details["multi"] >= 1]
LIST_DERIVATIVE_PARAMS: List[str] = [key for key, details in PARAMS_DICT.items() if details["multi"] == 2]
LIST_MOLECULAR_PARAMS: List[str] = [key for key, details in PARAMS_DICT.items() if details["multi"] == 3]
LIST_INSTRUMENTAL_PARAMS: List[str] = [key for key, details in PARAMS_DICT.items() if details["multi"] == 4]
# multi == 5: one value per LR instrument (all of them, no reference). Unlike
# LIST_INSTRUMENTAL_PARAMS (multi == 4, which expands to total_instruments_LR - 1
# because the first LR instrument is the offset reference), these expand to the
# full count total_instruments_LR. Used by beta_LR.
LIST_INSTRUMENTAL_FULL_PARAM: List[str] = [key for key, details in PARAMS_DICT.items() if details["multi"] == 5]
# Load molecular and condensed configurations
(molecs, params_list, start_molecs, start_elements, start_condensed_molecs,
ALL_CONDENSED_MOLECS_DICT, ALL_CONDENSED_MOLEC,
CONDENSED_NAMES, DICT_LABELS_CONDENSED_MOLEC) = _load_molecular_configs(
path_petitradtrans, PARAMS_LIST, LIST_ELEMENT_FOR_HYBRID, condensed_yaml)
# Named slice anchors for the params_list. These are the names whose
# canonical position equals the section starts that DOWNSTREAM CONSUMERS
# (LikelihoodHR, SpeciesMolec, ParamRegistry) actually slice on:
#
# - ``FIRST_MOLEC_NAME = "H2"``: the start of the molecule-like
# parameter section, which includes the background gases H2, He, H-,
# H, e- BEFORE the petitRADTRANS-scanned molecules. ModelData stores
# this as ``start_molec = get_index("H2")`` and forwards it to both
# SpeciesMolec and LikelihoodHR. Note this differs from the lower
# ``start_molecs`` integer below (which marks the petitRADTRANS scan
# start, ``"C2H2"``) — the integer is for internal params_list build
# only and no consumer slices on it.
# - ``FIRST_ELEMENT_NAME``: first hybrid-chemistry element (e.g. ``"C/H"``).
# - ``FIRST_CONDENSED_NAME``: first condensed species.
FIRST_MOLEC_NAME: str = "H2"
FIRST_ELEMENT_NAME: str = params_list[start_elements]
FIRST_CONDENSED_NAME: str = params_list[start_condensed_molecs]
# Update DICT_LABELS with all species
DICT_LABELS: Dict[str, str] = {key: val["label"] for key, val in PARAMS_DICT.items()}
DICT_LABELS.update(DICT_LABELS_MOLEC)
DICT_LABELS.update(DICT_LABELS_ELEMENTS)
DICT_LABELS.update(DICT_LABELS_CONDENSED_MOLEC)
# UI Color Configuration
# COLOR_MACRO: str = "#757575" # Material Gray 600
# COLOR_SUB_NOTEBOOK: str = "#9E9E9E" # Material Gray 500
# COLOR_SUB_NOTEBOOK_2: str = "#BDBDBD" # Material Gray 400
# COLOR_SUB_NOTEBOOK_3: str = "#E0E0E0" # Material Gray 300
COLOR_MACRO: str = "#E8B824"
COLOR_SUB_NOTEBOOK: str = "#FFD842"
COLOR_SUB_NOTEBOOK_2: str = "#FFE166"
COLOR_SUB_NOTEBOOK_3: str = "#FFEA8A"
# Colormap Configuration for Cross-Correlation Plots
# Hues ordered as rainbow spectrum (by HSV hue angle) + grey + matplotlib's inferno.
# Dark variants sit immediately after their bright counterpart at the same hue.
HUES: List[tuple] = [
('#FF0000', 'Red'), ('#8B0000', 'DarkRed'),
('#FF2200', 'Scarlet'), ('#FF4400', 'Vermillion'),
('#FF6600', 'Orange'), ('#FF8800', 'Amber'),
('#8C5511', 'DarkGold'), ('#FFAA00', 'Gold'),
('#FFCC00', 'Yellow'), ('#FFFF00', 'Lemon'),
('#CCFF00', 'Chartreuse'),
('#99FF00', 'LimeGreen'), ('#66FF00', 'SpringGreen'),
('#33FF00', 'Green'), ('#1F680D', 'DarkGreen'),
('#00FF33', 'Emerald'), ('#00FF66', 'Aquamarine'), ('#00FF99', 'Turquoise'),
('#00FFCC', 'Cyan'), ('#007777', 'DarkCyan'),
('#00CCFF', 'SkyBlue'), ('#0099FF', 'Azure'),
('#0066FF', 'Blue'), ('#0033FF', 'Cobalt'),
('#0000FF', 'Indigo'), ('#00008B', 'DarkBlue'),
('#3300FF', 'Violet'), ('#6600FF', 'Purple'),
('#9900FF', 'Amethyst'), ('#CC00FF', 'Magenta'),
('#A828B8', 'DarkPurple'), ('#8B008B', 'DarkMagenta'),
('#FF00CC', 'Fuchsia'), ('#FF0099', 'Rose'),
('#FF69B4', 'HotPink'), ('#FF0066', 'Cerise'), ('#FF0033', 'Crimson'),
('#808080', 'Grey'), ("inferno", "inferno"),
]
# Tab Configuration
LIST_TAB_MACRO = [
["General configuration", COLOR_MACRO],
["Run Retrievals", COLOR_MACRO],
["Retrieval Analysis and Cross Correlation", COLOR_MACRO],
["Telluric Removal", COLOR_MACRO],
["Forward Model", COLOR_MACRO],
["Cross Correlation Night-Forward", COLOR_MACRO],
["Create simulated HR data", COLOR_MACRO],
["DB and Data Interactions", COLOR_MACRO],
]
list_tab_analysis = [
["Posterior distribution", COLOR_SUB_NOTEBOOK],
["Model Plot", COLOR_SUB_NOTEBOOK],
["Cross Correlation Retrievals", COLOR_SUB_NOTEBOOK],
["Chains Analysis", COLOR_SUB_NOTEBOOK],
["Resume/Delete Retrievals", COLOR_SUB_NOTEBOOK],
]
list_tab_frame_parameter = [
["Planet + Data", COLOR_SUB_NOTEBOOK],
["Clouds/Haze", COLOR_SUB_NOTEBOOK],
["Temperatures", COLOR_SUB_NOTEBOOK],
["Temperatures2", COLOR_SUB_NOTEBOOK],
["VMR", COLOR_SUB_NOTEBOOK],
["Equilibrium", COLOR_SUB_NOTEBOOK],
["Molecules", COLOR_SUB_NOTEBOOK],
["Condensed elements", COLOR_SUB_NOTEBOOK],
]
list_tab_cross_correlation_1 = [
["Planetary Trace", COLOR_SUB_NOTEBOOK_2],
["Cross Correlation map", COLOR_SUB_NOTEBOOK_2],
]
list_tab_cross_correlation_2 = [
["Planetary Trace", COLOR_SUB_NOTEBOOK_2],
["Cross Correlation map", COLOR_SUB_NOTEBOOK_2],
["Likelihood map", COLOR_SUB_NOTEBOOK_2],
]
list_tab_simulated_hr = [
["Basic Configuration", COLOR_SUB_NOTEBOOK],
["Advanced Parameters", COLOR_SUB_NOTEBOOK],
]
list_tab_simulated_hr_advanced = [
["Atmospheric", COLOR_SUB_NOTEBOOK_2],
["Observational", COLOR_SUB_NOTEBOOK_2],
["Wavelength", COLOR_SUB_NOTEBOOK_2],
["Observatory", COLOR_SUB_NOTEBOOK_2],
]
# Filter Configuration
LIST_TELL_TABLE_FILTER: List[str] = ["None", "PCA_Scikit_Learn", "Eigenvalues"]
LIST_CHEMISTRY_TABLE_FILTER: List[str] = ["None", "Equilibrium", "Free Chemistry"]
LIST_PT_PROFILE_TABLE_FILTER: List[str] = ["None", "Isothermal", "Madhusudhan", "Guillot", "DoubleIsothermal", "FourNodeSpline"] + personalization_file["temperature_profile_list"]
LIST_SCATTERING_TABLE_FILTER: List[str] = ["None", "True", "False"]
LIST_RESOLUTION_TABLE_FILTER: List[str] = ["None", "High", "High+Low", "Low"]
LIST_ECC_OPI_TABLE_FILTER: List[str] = ["None", "True", "False"]
LIST_RETRIEVAL_MODE_FILTER: List[str] = ["None", "petitRADTRANS", "PyratBay"]
LIST_RAD_MODE_FILTER: List[str] = ["None", "Transmission", "Emission"]
LIST_SCATTERING_TABLE: List[str] = ["True", "False"]
LIST_ECC_OPI_TABLE: List[str] = ["True", "False"]
LIST_CORRELATION_METHODS = ["Numpy", "Weighted"]
# MCMC sampler choice. "DE-MC" (default) is the legacy byte-identical path;
# "Snooker" enables the DE-MCzs sampler (ter Braak and Vrugt 2008).
LIST_MCMC_SAMPLERS = ["DE-MC", "Snooker"]
LIST_RAD_MODE = ["Transmission", "Emission"]
LIST_CHEMISTRY_TABLE = ["Equilibrium", "Free Chemistry", "Hybrid Chemical Equilibrium"]
LIST_PT_PROFILE_TABLE = ["Isothermal", "Madhusudhan", "Guillot", "DoubleIsothermal", "FourNodeSpline"] + personalization_file["temperature_profile_list"]
# Fallback mapping: old PT profile names -> new names (for backward compatibility with saved configs/DB)
PT_PROFILE_FALLBACK: dict = {
"isot": "Isothermal",
"madhu": "Madhusudhan",
"guillot": "Guillot",
"personalized": "DoubleIsothermal",
}
# Reverse mapping: new PT profile names -> old names (for DB backward compatibility)
PT_PROFILE_REVERSE: dict = {v: k for k, v in PT_PROFILE_FALLBACK.items()}
[docs]
@staticmethod
def normalize_pt_profile(name: str) -> str:
"""Normalize a PT profile name, mapping old names to new ones if needed."""
return ConstantVariables.PT_PROFILE_FALLBACK.get(name, name)
[docs]
@staticmethod
def get_old_pt_profile_name(name: str) -> str | None:
"""Return the old PT profile name for backward-compatible DB queries, or None if no old name exists."""
return ConstantVariables.PT_PROFILE_REVERSE.get(name)
LIST_MODEL_REPRO = ["hard", "soft"]
LIST_STANDARDIZE_PCA = ["True", "False", "From pkl"]
LIST_LIMIT_PHASES = ["T14", "T23"]
LIST_TELL_TABLE = ["PCA_Scikit_Learn"] # , "Eigenvalues"]
LIST_RESOLUTION_TABLE = ["High", "High+Low", "Low"]
LIST_RETRIEVAL_MODE = ["petitRADTRANS", "PyratBay"]
LIST_RESOLUTION_DB = ["HR", "LR"]
# DataFrame Configuration
COLUMN_DF_GENERAL_INFO: List[str] = ["Variable", "Value"]
COLUMN_DF_PARAMETERS: List[str] = [
'name', 'is_present', 'molec', 'value', 'scale', 'range_min', 'range_max',
'rayleigh_species', 'in_bestpars', 'mass', 'sigma_prior',
"molec_formula", "constant_vmr", "isotope", "opacity_name_lr"
]
TYPES_DF_PARAMETERS: List[type] = [str, bool, str, float, float, float, float, bool, bool, float, float, str, bool, str, str]
# Folder Configuration
CC_FOLDER: str = "Models"
RESULTS_FOLDER: str = "Retrievals"
HR_INSTRUMENTS: str = "HR_Instruments"
LR_INSTRUMENTS: str = "LR_Instruments"
# Filter Table Configuration
DICT_FILTER_TABLE: Dict[str, Dict[str, Union[str, int, type]]] = {
"ID": {"name": "ID", "index": 0, "type": str},
"Fitted Params": {"name": "Fitted Params", "index": 1, "type": str},
"Priors": {"name": "Priors", "index": 2, "type": str},
"Order_Sel": {"name": "Order_Sel", "index": 3, "type": str},
"Nights": {"name": "Nights and instruments", "index": 4, "type": list},
"Method": {"name": "Method", "index": 5, "type": str},
"Rad Mode": {"name": "Rad Mode", "index": 6, "type": str},
"Exist": {"name": "Exist", "index": 7, "type": str},
"#modes": {"name": "#modes", "index": 8, "type": str},
"#atmos": {"name": "#atmos", "index": 9, "type": str},
"#scatterings": {"name": "#scatterings", "index": 10, "type": bool},
"#eccs": {"name": "#eccs", "index": 11, "type": bool},
"#resolutions": {"name": "#resolutions", "index": 12, "type": str},
"#Params": {"name": "#Params", "index": 13, "type": str},
"#Fixed Params": {"name": "#Fixed Params", "index": 14, "type": str},
"#Molec_EQ": {"name": "#Molec_EQ", "index": 15, "type": str},
"#PriorsComplete": {"name": "#PriorsComplete", "index": 16, "type": str},
"#Instruments": {"name": "#Instruments", "index": 17, "type": str}
}
LIST_FILTER_TABLE_COLUMNS: List[str] = [details["name"] for key, details in DICT_FILTER_TABLE.items()]
LIST_FILTER_TABLE_COLUMN_TYPES: List[type] = [details["type"] for key, details in DICT_FILTER_TABLE.items()]
LIST_OUTPUT_TABLE_COLUMN: tuple[str, ...] = ["PID", "Target", "Time", "ID", "Output"]
# Create colormap dictionary at module level (after class definition to avoid circular imports)
def _make_smooth_mono_cmap(color: str, name: str = 'custom'):
"""
Create a smooth monochromatic colormap with 5 control points.
This is a local copy to avoid circular import with general_functions.
"""
rgb = mcolors.to_rgb(color)
dark = tuple(c * 0.4 for c in rgb)
light = tuple(1 - (1 - c) * 0.4 for c in rgb)
return mcolors.LinearSegmentedColormap.from_list(
name,
['black', dark, color, light, 'white'],
N=256
)
# Generate colormaps from HUES, excluding "inferno" (which uses matplotlib's built-in)
ConstantVariables.CMAPS = {
h[1]: _make_smooth_mono_cmap(h[0], h[1])
for h in ConstantVariables.HUES
if "inferno" not in h
}
[docs]
def to_linear(name, value):
"""Convert a parameter value from log10 to linear if its YAML type is Log10ToLinear.
Reads the parameter type from the canonical PARAMS_DICT loaded from parameters.yaml.
Parameters whose type is not Log10ToLinear are returned unchanged. None passes through
unchanged so that optional parameters (not fitted in a given retrieval) remain None.
Args:
name: Parameter name as it appears in parameters.yaml.
value: Raw sampled value (scalar, numpy scalar, numpy array, or Python
list/tuple for multi-valued parameters). May be None.
Returns:
10**value if the parameter type is Log10ToLinear, value unchanged otherwise,
or None if value is None. For list/tuple inputs the conversion is applied
element-wise and a list is returned (10 ** a Python list is unsupported).
"""
if value is None:
return None
if ConstantVariables.PARAMS_DICT[name]["type"] != "Log10ToLinear":
return value
if isinstance(value, (list, tuple)):
# Multi-valued parameters (e.g. sf_multi, f_rot) arrive as Python lists,
# which do not support the ** operator: convert element-wise.
return [10 ** v for v in value]
return 10 ** value