"""Configuration module."""
# gCO2eq/kWh - source: https://ourworldindata.org/grapher/carbon-intensity-electricity Global Average
# Currency/kWh (Euro) - source: https://www.stromauskunft.de/strompreise/ 03.05.2023
import configparser
import logging
import os
import pprint as pp
from pathlib import Path
from typing import Any, Mapping
from perun.io.io import IOFormat
log = logging.getLogger(__name__)
_default_config: Mapping[str, Mapping[str, Any]] = {
"post-processing": {
"power_overhead": 0, # Watt
"pue": 1.0, # No assumption where the workflow is running
"emissions_factor": 230.0, # gCO2eq/kWh - Germany, 20.05.2025 (https://app.electricitymaps.com/zone/DE/72h/hourly)
"price_factor": 0.2678, # Euro/kWh - Germany, 20.05.2025 (https://www.stromauskunft.de/strompreise/)
"price_unit": "€",
},
"monitor": {
"sampling_period": 1,
"include_backends": "",
"include_sensors": "",
"exclude_backends": "",
"exclude_sensors": "",
},
"output": {
"app_name": None,
"run_id": None,
"format": "text",
"data_out": "./perun_results",
},
"benchmarking": {
"rounds": 1,
"warmup_rounds": 0,
"metrics": "runtime,energy",
"region_metrics": "runtime,power",
},
"benchmarking.units": {
"joule": "k",
"second": "",
"percent": "",
"watt": "",
"byte": "G",
},
"debug": {"log_lvl": "WARNING", "log_file": None},
}
config: configparser.ConfigParser = configparser.ConfigParser(allow_no_value=True)
config.read_dict(_default_config)
globalConfigPath = Path.home() / ".config/perun.ini"
if globalConfigPath.exists() and globalConfigPath.is_file():
config.read(globalConfigPath)
localConfigPath = Path.cwd() / ".perun.ini"
if globalConfigPath.exists() and globalConfigPath.is_file():
config.read(globalConfigPath)
[docs]
def read_custom_config(
pathStr: str,
) -> None:
"""Read INI config file and save in global configuration objects.
Parameters
----------
pathStr : str
Path to configuration file.
"""
configPath: Path = Path(pathStr)
if configPath.exists() and configPath.is_file():
config.read(configPath)
[docs]
def save_to_config(key: str, value: Any) -> None:
"""Save key and value to configuration.
Parameters
----------
key : str
Option name
value : Any
Option value
"""
for section in config.sections():
if config.has_option(section, key):
config.set(section, key, str(value))
[docs]
def read_environ() -> None:
"""Read perun environmental variables."""
for section, subconf in _default_config.items():
for option in subconf.keys():
envvar = f"PERUN_{option.upper()}"
if envvar in os.environ:
config.set(section, option, os.environ[envvar])
[docs]
def sanitize_config(config: configparser.ConfigParser) -> configparser.ConfigParser:
"""Sanitize configuration values.
Parameters
----------
config : configparser.ConfigParser
Configuration object.
Returns
-------
configparser.ConfigParser
Sanitized configuration object.
"""
# Ensure post processing variables are valid
try:
power_overhead = config.getfloat("post-processing", "power_overhead")
if power_overhead < 0:
log.warning(
f"Invalid power overhead {power_overhead}. Should be a number higher or equal than 0. Defaulting to 0."
)
config.set("post-processing", "power_overhead", "0")
except ValueError:
log.warning(
"Invalid power overhead. Should be a number higher or equal than 0. Defaulting to 0."
)
config.set("post-processing", "power_overhead", "0")
try:
pue = config.getfloat("post-processing", "pue")
if pue < 1:
log.warning(
f"Invalid PUE {pue}. Should be a number higher or equal than 1. Defaulting to 1."
)
config.set("post-processing", "pue", "1.0")
except ValueError:
log.warning(
"Invalid PUE. Should be a number higher or equal than 1. Defaulting to 1."
)
config.set("post-processing", "pue", "1.0")
try:
default_emissions_factor = _default_config["post-processing"][
"emissions_factor"
]
emissions_factor = config.getfloat("post-processing", "emissions_factor")
if emissions_factor < 0:
# Default value is the global average emissions factor
log.warning(
f"Invalid emissions factor {emissions_factor}. Should be a number higher or equal than 0. Defaulting to {default_emissions_factor} gCO2eq/kWh."
)
config.set(
"post-processing", "emissions_factor", str(default_emissions_factor)
)
except ValueError:
log.warning(
f"Invalid emissions factor. Should be a number higher or equal than 0. Defaulting to {default_emissions_factor} gCO2eq/kWh."
)
config.set("post-processing", "emissions_factor", str(default_emissions_factor))
try:
default_price_factor = _default_config["post-processing"]["price_factor"]
price_factor = config.getfloat("post-processing", "price_factor")
if price_factor < 0:
log.warning(
f"Invalid price factor {price_factor}. Should be a number higher or equal than 0. Defaulting to {default_price_factor} Currency/kWh."
)
config.set("post-processing", "price_factor", str(default_price_factor))
except ValueError:
log.warning(
"Invalid price factor. Should be a number higher or equal than 0. Defaulting to 0.3251 Currency/kWh."
)
config.set(
"post-processing",
"price_factor",
str(_default_config["post-processing"]["price_factor"]),
)
# Ensure that the monitoring options are valid
try:
sampling_period = config.getfloat("monitor", "sampling_period")
if sampling_period < 0.1:
log.warning(
f"Invalid sampling period {sampling_period}. Should be a number higher than 0.1 . Defaulting to 1."
)
config.set("monitor", "sampling_period", "1")
except ValueError:
log.warning(
"Invalid sampling period. Should be a number higher than 0.1 . Defaulting to 1."
)
config.set("monitor", "sampling_period", "1")
# Ensure only the include or exclude options are set
include_backends = config.get("monitor", "include_backends")
include_sensors = config.get("monitor", "include_sensors")
exclude_backends = config.get("monitor", "exclude_backends")
exclude_sensors = config.get("monitor", "exclude_sensors")
if include_backends and exclude_backends:
log.warning(
"Both include and exclude backends options are set. Defaulting to exclude only."
)
config.set("monitor", "include_backends", "")
if include_sensors and exclude_sensors:
log.warning(
"Both include and exclude sensors options are set. Defaulting to exclude only."
)
config.set("monitor", "include_sensors", "")
# Ensure that the output format is valid
try:
format = config.get("output", "format")
IOFormat(format)
except ValueError:
log.warning(
f"Invalid output format {format}. Defaulting to text. Avilable formats: {pp.pformat([f.value for f in IOFormat])}"
)
config.set("output", "format", IOFormat.TEXT.value)
# Ensure that the rounds and warmup rounds are valid
try:
rounds = config.getint("benchmarking", "rounds")
if rounds < 1:
log.warning(
f"Invalid number rounds {rounds}. Should be a number higher than 1. Defaulting to 1."
)
config.set("benchmarking", "rounds", "1")
except ValueError:
log.warning(
"Invalid number rounds. Should be a number higher than 1. Defaulting to 1."
)
config.set("benchmarking", "rounds", "1")
try:
warmup_rounds = config.getint("benchmarking", "warmup_rounds")
if warmup_rounds < 0:
log.warning(
f"Invalid number warmup rounds {warmup_rounds}. Should be a number higher than 0. Defaulting to 0."
)
config.set("benchmarking", "warmup_rounds", "0")
except ValueError:
log.warning(
"Invalid number warmup rounds. Should be a number higher than 0. Defaulting to 0."
)
config.set("benchmarking", "warmup_rounds", "0")
# Ensure that the log_lvl is valid
log_lvl = config.get("debug", "log_lvl")
if log_lvl not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
log.warning(f"Invalid log level {log_lvl}. Defaulting to WARNING.")
config.set("debug", "log_lvl", "WARNING")
return config