import os
import json
import logging
import numpy as np
import pandas as pd
from pathlib import Path
from ForMoSA.core.enums import ParameterKind
from ForMoSA.core.errors import ForMoSAError
from ForMoSA.core.loggings import setup_logging
from ForMoSA.parameter.parameter import Parameter
from ForMoSA.config.global_config import ConfigParameters
[docs]
class ParameterSet(object):
"""
Container for nested sampling parameters.
Parameters
----------
logger : logging.Logger
Logger
log_level : str
Level of the logging
Notes
-----
Authors: Allan Denis
"""
def __init__(self, logger: logging.Logger | None = None, log_level: str = 'INFO') -> None:
self._parameters: list[Parameter] = []
self._logger = logger if logger is not None else setup_logging(level=log_level, name='ParameterSet')
# ===========================
# Representation
# ===========================
def __repr__(self):
return f"<ParameterSet: {len(self.parameters)} parameters>"
# ===========================
# Properties
# ===========================
@property
def logger(self) -> logging.Logger:
"""Logger."""
return self._logger
@property
def parameters(self) -> list[Parameter]:
"""List of parameters."""
return self._parameters
@property
def free_parameters(self) -> list[Parameter]:
"""list of free parameters."""
return [p for p in self.parameters if not p.is_fixed]
@property
def fixed_parameters(self) -> list[Parameter]:
"""list of fixed parameters."""
return [p for p in self.parameters if p.is_fixed]
@property
def names(self) -> list[str]:
"""list of parameter names."""
return [p.name for p in self.parameters]
@property
def free_names(self) -> list[str]:
"""list of parameter names."""
return [p.name for p in self.free_parameters]
@property
def titles(self) -> list[str]:
"""List of titles."""
return [p.title for p in self.parameters]
@property
def free_titles(self) -> list[str]:
"""List of titles."""
return [p.title for p in self.free_parameters]
@property
def kinds(self) -> list[ParameterKind]:
"""list of parameter kinds."""
return [p.kind for p in self.parameters]
@property
def n_free_parameters(self) -> int:
"""Number of free parameters."""
return len(self.free_parameters)
@property
def to_dict(self) -> dict:
"""Dictionary representation of the set of parameters."""
data = {}
for i, name in enumerate(self.names):
data[name] = self.parameters[i].to_dict
return data
# ==========================
# Class methods
# ==========================
[docs]
@classmethod
def from_config(cls, config_params: ConfigParameters, logger: logging.Logger | None = None, log_level: str = 'INFO') -> "ParameterSet":
'''
Generate instance of ParameterSet from an instance of ConfigParameters.
Parameters
----------
config_params : ConfigParameters
Instance of ConfigParameters
logger : logging.Logger
Logger
log_level : str
Level of the logging
Returns
-------
"ParameterSet"
As instance of ParameterSet
Notes
-----
Authors: Allan Denis
'''
logger = logger or setup_logging(level=log_level, name='ParameterSet')
if not isinstance(config_params, ConfigParameters):
raise ForMoSAError(f'Wrong type for config_params: {type(config_params)}. Expected a ConfigParameters', logger)
logger.debug('Generating set of parameters from configuration file')
params = cls(logger=logger)
for name, value in config_params.to_dict.items():
try:
param = config_params._parse_param(name, value, logger=params.logger)
except ForMoSAError as e:
raise ForMoSAError(e, logger)
if param is not None:
logger.info(f' Detected {param.kind.kind} Parameter with name {param.name} from config')
params.add_parameter(param)
logger.info(' Set of parameters generated')
return params
[docs]
@classmethod
def from_dict(cls, data: dict, logger: logging.Logger | None = None, log_level: str = 'INFO') -> 'ParameterSet':
'''
Reconstruct a ParameterSet from a dictionary of ParameterSet.
Parameters
----------
data : dict
Dictionary containing ParameterSet parameters
logger : logging.Logger
Logger
log_level : str
Level of the logging
Returns
-------
'ParameterSet'
An instance of class ParameterSet
Notes
-----
Authors: Allan Denis
'''
logger = logger if logger is not None else setup_logging(level=log_level, name='ParameterSet')
param_set = cls(logger=logger)
logger.debug('Building instance of ParameterSet from dictionary')
for name in data.keys():
param = Parameter.from_dict(data=data[name], logger=logger)
param_set.add_parameter(param)
return param_set
[docs]
@classmethod
def from_json(cls, path: str | os.PathLike, logger: logging.Logger | None = None, log_level: str = 'INFO') -> 'ParameterSet':
'''
Reconstruct a ParameterSet from a json file.
Parameters
----------
path : str | os.PathLike
Path to the json file
logger : logging.Logger
Logger
log_level : str
Level of the logging
Returns
-------
'ParameterSet'
An instance of class ParameterSet
Notes
-----
Authors: Allan Denis
'''
logger = logger if logger is not None else setup_logging(level=log_level, name='ParameterSet')
if not isinstance(path, (str, os.PathLike)):
raise ForMoSAError(f'Wrong type for path: {type(path)}. Expected a string or os.PathLike', logger)
logger.debug(f'Build instance of ParameterSet from json file {str(path) + "parameters.json"}')
filepath = Path(str(path) + 'parameters.json')
if not filepath.exists():
raise ForMoSAError(f'{filepath} does not exist')
with open(filepath, "r") as f:
data = json.load(f)
return cls.from_dict(data, logger=logger)
# ==========================
# Methods
# ==========================
[docs]
def add_parameter(self, parameter: Parameter) -> None:
'''
Add a Parameter to the set.
Parameters
----------
parameter : Parameter
Instance of class Parameter to add
Notes
-----
Authors: Allan Denis
'''
if not isinstance(parameter, Parameter):
raise ForMoSAError("Parameter must be a Parameter instance")
if any(p.name == parameter.name for p in self.parameters):
raise ForMoSAError(f"Parameter '{parameter.name}' already exists")
self.logger.info(f' Adding {parameter.kind.kind} Parameter with name {parameter.name} to the set of parameters')
self.parameters.append(parameter)
[docs]
def summary(self, as_dataframe: bool = True) -> pd.DataFrame | str:
'''
Return a summary of parameters.
Parameters
----------
as_dataframe : bool
If True, return pandas.DataFrame, else formatted string
Returns
-------
pandas.DataFrame | str
Summary of parameters
Notes
-----
Authors: Allan Denis
'''
rows = []
for p in self.parameters:
rows.append({
"name": p.name,
"kind": p.kind.name,
"prior": p.prior.prior_type.value,
"value": (f"{p.prior.value}" if p.is_fixed else f"{p.prior.bounds}"),
"fixed": p.is_fixed,
"scope": p.scope,
"obs_index": str(p.obs_index)
})
if as_dataframe:
return pd.DataFrame(rows)
# Text summary
header = f"{'Name':<15} {'Kind':<15} {'Prior':<15} {'Value':<20} {'Fixed':<15} {'Scope':<15} {'Obs_index':<15}"
sep = "-" * len(header)
lines = [header, sep]
for r in rows:
lines.append(f"{r['name']:<15} {r['kind']:<15} {r['prior']:<15} {r['value']:<20} {r['fixed']:<15} {r['scope']:<15} {r['obs_index']:<15}")
return ('').join(lines)
[docs]
def to_json(self, path: str | os.PathLike) -> None:
'''
Save the set of parameters to a given path as a json file.
Parameters
----------
path : str | os.PathLike
Path to save the set of parameters
Notes
-----
Authors: Allan Denis
'''
if not isinstance(path, (str, os.PathLike)):
raise ForMoSAError(f'Wrong type for path: {type(path)}. Expected a string or os.PathLike', self.logger)
self.logger.info(f'Save set of parameters to {path}')
if not os.path.exists(path):
self.logger.warning(f'{path} does not exist. Creating it.')
Path(path).mkdir(exist_ok=True, parents=True)
with open(str(path) + 'parameters.json', 'w') as f:
json.dump(self.to_dict, f, indent=4)