import os
from pathlib import Path
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from dataclasses import dataclass, field
from matplotlib import colors as mcolors
from ForMoSA.core.errors import ForMoSAError
# Default filter path (can be overridden by user)
FILTER_PATH = Path.home() / 'filters'
[docs]
def set_filter_path(path: str | os.PathLike) -> None:
'''
Set the global path used to store photometry filters.
Parameters
----------
path : str | os.PathLike
Path to the filters folder
Notes
-----
Authors: Allan Denis
'''
global FILTER_PATH
FILTER_PATH = Path(path).expanduser().resolve()
[docs]
def darken_color(color: str, factor: float = 0.7) -> str:
'''
Darken a matplotlib color.
Parameters
----------
color : str
Any matplotlib-compatible color.
factor : float
Multiplicative factor (<1 darker, >1 lighter)
Returns
-------
str : hex color
Notes
-----
Authors: Allan Denis
'''
rgb = mcolors.to_rgb(color)
dark_rgb = tuple(max(0, min(1, c * factor)) for c in rgb)
return mcolors.to_hex(dark_rgb)
# ==================================================
# General plotting configuration
# ==================================================
[docs]
@dataclass
class MainPlotConfig:
'''
Dataclass to handle main parameters for plotting.
Notes
-----
Authors: Allan Denis
'''
figsize: tuple[float, float] = (10.0, 7.0)
legend_ncol: int = 1
legend_hc_ncol: int = 1
legend_filt_ncol: int = 1
legend_fontsize: str = "small"
MAIN_PLOT = MainPlotConfig()
# ==================================================
# Spectroscopic plotting configuration
# ==================================================
[docs]
@dataclass
class ObsPlotConfig:
'''
Dataclass to handle configurations for plotting data.
Notes
-----
Authors: Allan Denis
'''
# --- Color management
cmap: mcolors.Colormap = field(default_factory = lambda: cm.jet)
color: str = "red"
edgecolor: str = None
norm: mcolors.Normalize = field(default_factory = lambda: plt.Normalize(vmin=1.0, vmax=15))
# --- Marker
marker: str = "s"
markersize: int = 40
linewidth: float = 1
# --- Errorbar
errorbar_fmt: str = "None"
errorbar_alpha: float = 1.0
errorbar_capsize: float = 4.0
# --- zorder
zorder_data: int = 3
zorder_error: int = 1
# ---- Legend
label: bool = True
def __post_init__(self):
if self.edgecolor is None or self.edgecolor == "auto":
self.edgecolor = darken_color(self.color)
[docs]
def to_dict(self) -> dict:
'''
Return configuration options as a dictionary.
Notes
-----
Authors: Allan Denis
'''
return {k: v for k, v in self.__dict__.items() if v is not None}
[docs]
def set_plot_config(self, **kwargs) -> None:
'''
Update global default plotting parameters for spectroscopic plots.
Parameters
----------
**kwargs : dict
Keyword arguments to override attributes of the config
Notes
-----
Authors: Allan Denis
'''
for key, value in kwargs.items():
if not hasattr(self, key):
raise ForMoSAError(f'Unknown spectral plot config key: {key}')
setattr(self, key, value)
self.edgecolor = darken_color(self.color)
[docs]
@dataclass
class SpectralPlotConfig(ObsPlotConfig):
'''
Dataclass to handle configurations for plotting spectroscopic data.
Notes
-----
Authors: Allan Denis
'''
color: str = "red"
marker: str = "None"
# ==================================================
# Photometric plotting configuration
# ==================================================
[docs]
@dataclass
class PhotometricPlotConfig(ObsPlotConfig):
'''
Dataclass to handle configurations for plotting photometric data.
Notes
-----
Authors: Allan Denis
'''
color: str = "blue"
markersize: int = 70
linewidth: float = 2.0
label_filter: bool = False
label_data: bool = True
# ==================================================
# CornerPlot plotting configuration
# ==================================================
[docs]
@dataclass
class CornerPlotConfig:
'''
Dataclass to handle configurations for corner plots.
Notes
-----
Authors: Allan Denis
'''
figsize: tuple[float, float] = (15.0, 15.0)
color: str = "magenta"
bins: int = 80
smooth: float = 1
smooth1d: float | None = None
plot_datapoints: bool = False
plot_density: bool = True
plot_contours: bool = True
fill_contours: bool = True
quantiles: tuple = (0.16, 0.5, 0.84)
levels: list = field(default_factory = lambda: [0.997, 0.95, 0.68])
show_titles: bool = True
title_fmt: str = " .2f"
hist_kwargs: dict | None = None
contour_kwargs: dict | None = None
contour_kwargs: dict = field(default_factory = lambda: dict(colors='magenta', linewidths=0.7))
pcolor_kwargs: dict = field(default_factory = lambda: dict(color='red'))
title_kwargs: dict = field(default_factory = lambda: dict(fontsize=14))
label_kwargs: dict = field(default_factory= lambda: dict(fontsize=14))
max_n_ticks: int = 4
@property
def to_dict(self) -> dict:
'''
Return configuration options as a dictionary.
Notes
-----
Authors: Allan Denis
'''
return {k: v for k, v in self.__dict__.items() if v is not None}
[docs]
def set_corner_plot_config(self, **kwargs) -> None:
'''
Update global default plotting parameters for corner plot.
Parameters
----------
**kwargs : dict
Keyword arguments to override attributes of the config
Notes
-----
Authors: Allan Denis
'''
for key, value in kwargs.items():
if not hasattr(self, key):
raise ForMoSAError(f'<Unknown corner plot config key: {key}>')
setattr(self, key, value)
CORNER_PLOT = CornerPlotConfig()
# ==================================================
# Chains plotting configuration
# ==================================================
[docs]
@dataclass
class ChainsPlotConfig:
'''
Dataclass to handle configurations for chains plot.
Notes
-----
Authors: Allan Denis
'''
figsize: tuple[float, float] = (15.0, 15.0)
color_chains: str = 'magenta'
alpha_chains: float = 0.8
color_plot_burn_in: str = 'red'
fontsize_burn_in: int = 14
text_burn_in: tuple[float, float] = 0.8, 0.8
color_text_burn_in: str = 'red'
linestyle_burn_in: str = '--'
show_weights: bool = True
color_plot_weights: str = 'black'
fontsize_weights: int = 14
alpha_weights: float = 0.4
text_weights: tuple[float, float] = 0.8, 0.7
color_text_weights: str = 'grey'
plot_best_value: bool = True
color_best_value: str = 'black'
linestyle_best_value: str = '--'
@property
def to_dict(self) -> dict:
'''
Return configuration options as a dictionary.
Notes
-----
Authors: Allan Denis
'''
return {k: v for k, v in self.__dict__.items() if v is not None}
[docs]
def set_chains_plot_config(self, **kwargs) -> None:
'''
Update global default plotting parameters for chains plot.
Parameters
----------
**kwargs : dict
Keyword arguments to override attributes of the config
Notes
-----
Authors: Allan Denis
'''
for key, value in kwargs.items():
if not hasattr(self, key):
raise ForMoSAError(f'<Unknown chains plot config key: {key}>')
setattr(self, key, value)
CHAINS_PLOT = ChainsPlotConfig()
# ==================================================
# Radar plotting configuration
# ==================================================
[docs]
@dataclass
class RadarPlotConfig:
'''
Dataclass to handle configurations for radar plot.
Notes
-----
Authors: Allan Denis
'''
figsize: tuple[float, float] = (6.0, 6.0)
color_radar: str = 'magenta'
linewidth: float = 2.0
fontsize_names: int = 12
fontisze_ticks: int = 8
color_ticks: str = 'black'
alpha_fill: float = 0.2
quantiles: tuple[float, float] = (0.16, 0.84)
size_quantiles: int = 20
color_quantiles: str = 'black'
@property
def to_dict(self) -> dict:
'''
Return configuration options as a dictionary.
Notes
-----
Authors: Allan Denis
'''
return {k: v for k, v in self.__dict__.items() if v is not None}
[docs]
def set_radar_plot_config(self, **kwargs) -> None:
'''
Update global default plotting parameters for radar plot.
Parameters
----------
**kwargs : dict
Keyword arguments to override attributes of the config
Notes
-----
Authors: Allan Denis
'''
for key, value in kwargs.items():
if not hasattr(self, key):
raise ForMoSAError(f'<Unknown radar plot config key: {key}>')
setattr(self, key, value)
RADAR_PLOT = RadarPlotConfig()
# ==================================================
# Best fit plotting configuration
# ==================================================
[docs]
@dataclass
class BestFitPlotConfig:
'''
Dataclass to handle configurations for best fit plot.
Notes
-----
Authors: Allan Denis
'''
color: str = 'black'
linewidth: float = 2.0
zorder: int = 100
@property
def to_dict(self) -> dict:
'''
Return configuration options as a dictionary.
Notes
-----
Authors: Allan Denis
'''
return {k: v for k, v in self.__dict__.items() if v is not None}
[docs]
def set_best_fit_plot_config(self, **kwargs) -> None:
'''
Update global default plotting parameters for best fit plot.
Parameters
----------
**kwargs : dict
Keyword arguments to override attributes of the config
Notes
-----
Authors: Allan Denis
'''
for key, value in kwargs.items():
if not hasattr(self, key):
raise ForMoSAError(f'<Unknown best fit plot config key: {key}>')
setattr(self, key, value)
BEST_FIT_PLOT = BestFitPlotConfig()
[docs]
@dataclass
class PlotsConfig:
'''
Dataclass to handle configurations for plots.
Notes
-----
Authors: Allan Denis
'''
CornerPlot: CornerPlotConfig = field(default_factory = lambda: CORNER_PLOT)
ChainsPlot: ChainsPlotConfig = field(default_factory = lambda: CHAINS_PLOT)
RadarPlot: RadarPlotConfig = field(default_factory = lambda: RADAR_PLOT)
BestFitPlot: BestFitPlotConfig = field(default_factory = lambda: BEST_FIT_PLOT)
def __post_init__(self):
for name, instance in zip(['CornerPlot', 'ChainsPlot', 'RadarPlot', 'BestFitPlot'], [CornerPlotConfig, ChainsPlotConfig, RadarPlotConfig, BestFitPlotConfig]):
value = getattr(self, name)
if not isinstance(value, instance):
raise ForMoSAError(f'Wrong type for {name}. Expected {instance}')
@property
def to_dict(self) -> dict:
data = {}
for name in ['CornerPlot', 'ChainsPlot', 'RadarPlot', 'BestFitPlot']:
value = getattr(self, name).to_dict
data[name] = value
return data
PLOTS_CONFIG = PlotsConfig()