Source code for GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables

"""
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)
[docs] @staticmethod def figure_geometry(scale: float = 2.0) -> str: """Return a centered geometry string for figure/plot windows. The size is ``scale`` times the base window (so plot windows open larger than the main GUI), clamped to 95% of the screen so it never overflows. Args: scale: Multiplier applied to the base window size. Defaults to 2.0 (twice the base window). Returns: A Tk geometry string ``"<w>x<h>+<x>+<y>"``. """ sw = ConstantVariables.screen_width sh = ConstantVariables.screen_height width = min(int(ConstantVariables.WINDOW_WIDTH * scale), int(sw * 0.95)) height = min(int(ConstantVariables.WINDOW_HEIGHT * scale), int(sh * 0.95)) pos_x = max(0, (sw - width) // 2) pos_y = max(0, (sh - height) // 2) return f"{width}x{height}+{pos_x}+{pos_y}"
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