Skip to content

Config

Module functions for working with configuration files.

reconcile_config_args

reconcile_config_args(args, default_config)

Reconcile command line arguments with the default configuration.

Command line arguments take precedence over the default configuration. If a partial configuration file is specified (with '-c' or '--config-file') the defaults are over-ridden by these values (internally the configuration dictionary is updated with these values). Any other command line arguments take precedence over both the default and those supplied in a configuration file (again the dictionary is updated).

The final configuration is validated before processing begins.

Parameters:

Name Type Description Default
args Namespace

Command line arguments passed into LayOpt.

required
default_config dict[str, Any]

Dictionary containing the default configuration for the package.

required

Returns:

Type Description
dict[str, Any]

The configuration dictionary.

Source code in src/layopt/config.py
def reconcile_config_args(
    args: Namespace | None, default_config: dict[str, Any]
) -> dict[str, Any]:
    """
    Reconcile command line arguments with the default configuration.

    Command line arguments take precedence over the default configuration. If a partial configuration file is specified
    (with '-c' or '--config-file') the defaults are over-ridden by these values (internally the configuration
    dictionary is updated with these values). Any other command line arguments take precedence over both the default and
    those supplied in a configuration file (again the dictionary is updated).

    The final configuration is validated before processing begins.

    Parameters
    ----------
    args : Namespace
        Command line arguments passed into LayOpt.
    default_config : dict[str, Any]
        Dictionary containing the default configuration for the package.

    Returns
    -------
    dict[str, Any]
        The configuration dictionary.
    """
    # If we have args check for 'config_file', if present load and merge with default_config
    if args is not None and args.config_file is not None:
        logger.debug(
            f"BEFORE update with config_file args :\n{pformat(vars(args), indent=4)}"
        )
        logger.debug(
            f"BEFORE update with config_file default_config :\n{pformat(default_config, indent=4)}"
        )
        config = read_yaml(str(args.config_file))
        config = merge_mappings(map1=default_config, map2=config)
    # If no args we use the default_config
    else:
        config = default_config
        # Convert fields to numpy arrays
        for to_convert in ["loaded_points", "support_points", "filter_levels"]:
            if to_convert in config:
                config = _convert_to_numpy_array(config=config, to_convert=to_convert)

    # Override the config with command line arguments
    if args is not None:
        _args = vars(args)
        # Remove args that are not part of the configuration
        _args.pop("config_file")
        _args.pop("func")
        _args.pop("module")
        logger.debug(
            f"BEFORE update from command line args :\n{pformat(_args, indent=4)}"
        )
        logger.debug(
            f"BEFORE update from command line config :\n{pformat(default_config, indent=4)}"
        )
        config = merge_mappings(map1=config, map2=_args)

    # If user has specified a solver we update the value outside of merge_mappings() because args["solver"] is not
    # nested as it is in the default at config["cvxpy"]["solver"]
    if "solver" in _args and _args["solver"] is not None:
        config["cvxpy"]["solver"] = _args["solver"].upper()
    # Pass config through CVXPY solver name validation
    config = reconcile_solver(config)
    logger.debug(f"Final configuration AFTER update : \n{pformat(config, indent=4)}\n")
    return dict(config)

merge_mappings

merge_mappings(map1, map2)

Merge two mappings (dictionaries), with priority given to the second mapping.

map1 is updated with values from map2.

Parameters:

Name Type Description Default
map1 MutableMapping[Any, Any]

First mapping to merge, with secondary priority.

required
map2 MutableMapping[Any, Any]

Second mapping to merge, with primary priority.

required

Returns:

Type Description
dict

Merged dictionary.

Source code in src/layopt/config.py
def merge_mappings(
    map1: MutableMapping[Any, Any], map2: MutableMapping[Any, Any]
) -> dict[str, Any]:
    """
    Merge two mappings (dictionaries), with priority given to the second mapping.

    ``map1`` is updated with values from ``map2``.

    Parameters
    ----------
    map1 : MutableMapping[Any, Any]
        First mapping to merge, with secondary priority.
    map2 : MutableMapping[Any, Any]
        Second mapping to merge, with primary priority.

    Returns
    -------
    dict
        Merged dictionary.
    """
    for key, value in map2.items():
        # Recurse if we have a MutableMapping (e.g. nested dictionary)
        if isinstance(value, MutableMapping):
            map1[key] = merge_mappings(map1.get(key, {}), value)
        # Otherwise update the value
        elif value is not None:
            logger.debug(f"key  : {key=}")
            map1[key] = value
            logger.debug(f"map1 : {map1[key]=}")
            logger.debug(f"map2 : {value=}")
    # Tidy up variables
    if "base_dir" in map1:
        map1["base_dir"] = (
            Path("./") if map1["base_dir"] is None else convert_path(map1["base_dir"])
        )
    if "output_dir" in map1:
        map1["output_dir"] = convert_path(map1["output_dir"])
    if "load_direction" in map1:
        map1["load_direction"] = (
            map1["load_direction"][0],
            map1["load_direction"][1],
        )
    # Convert to numpy arrays
    for to_convert in ["loaded_points", "support_points", "filter_levels"]:
        if to_convert in map1:
            map1 = _convert_to_numpy_array(config=map1, to_convert=to_convert)
    if "csv_filename" in map1:
        map1["csv_filename"] = (
            (f"results_{get_date_time(strftime='%Y-%m-%d-%H%M%S')}.csv")
            if map1["csv_filename"] == "results.csv"
            else map1["csv_filename"]
        )
    return dict(map1)

reconcile_solver

reconcile_solver(config)

Check solver name in config is installed for use in CVXPY.

Parameters:

Name Type Description Default
config dict[str, Any]

Dictionary containing requested CVXPY solver name.

required

Returns:

Type Description
dict[str, Any]

Dictionary with reconciled CVXPY solver name.

Source code in src/layopt/config.py
def reconcile_solver(config: dict[str, Any]) -> dict[str, Any]:
    """
    Check solver name in config is installed for use in CVXPY.

    Parameters
    ----------
    config : dict[str, Any]
        Dictionary containing requested CVXPY solver name.

    Returns
    -------
    dict[str, Any]
        Dictionary with reconciled CVXPY solver name.
    """
    # use MOSEK if solver not specified in config
    requested_solver = str(config["cvxpy"].get("solver", "mosek")).upper()
    cvxpy_solvers = cvx.installed_solvers()
    if requested_solver in cvxpy_solvers:
        logger.info(f"Using available CVXPY solver: {requested_solver}")
        config["cvxpy"]["solver"] = requested_solver
        return config

    logger.warning(f"Requested CVXPY solver '{requested_solver}' is not installed.")
    # check if MOSEK installed, and use CLARABEL as back-up if not, else let CVXPY choose solver
    if "MOSEK" in cvxpy_solvers:
        logger.info("Falling back to available solver: 'MOSEK'")
        config["cvxpy"]["solver"] = "MOSEK"
    elif "CLARABEL" in cvxpy_solvers:
        logger.info("Falling back to available solver: 'CLARABEL'")
        config["cvxpy"]["solver"] = "CLARABEL"
    else:
        logger.error(
            "Fallback solver 'CLARABEL' not found. Leaving empty to let CVXPY auto-select."
        )
        config["cvxpy"].pop("solver", None)
    return config