From b6d3c3a3ca243d542cf080598a41ca0226ea9de5 Mon Sep 17 00:00:00 2001 From: voetberg Date: Fri, 5 Apr 2024 12:27:48 -0500 Subject: [PATCH] Base for plotting --- src/metrics/coverage_fraction.py | 23 +++++---- src/models/sbi_model.py | 2 +- src/plots/__init__.py | 8 +++- src/plots/cdf_ranks.py | 35 ++++++++++++++ src/plots/coverage_fraction.py | 78 +++++++++++++++++++++++++++++++ src/plots/plot.py | 48 +++++++++++-------- src/plots/ranks.py | 37 +++++++++++++++ src/utils/defaults.py | 13 ++++-- tests/conftest.py | 15 +++--- tests/plots/coverage.pdf | Bin 0 -> 16720 bytes tests/plots/sbc_ranks.pdf | Bin 0 -> 13546 bytes tests/plots/sbc_ranks_cdf.pdf | Bin 0 -> 36086 bytes tests/test_plots.py | 56 ++++++++++++++++++++++ 13 files changed, 271 insertions(+), 44 deletions(-) create mode 100644 src/plots/cdf_ranks.py create mode 100644 src/plots/coverage_fraction.py create mode 100644 src/plots/ranks.py create mode 100644 tests/plots/coverage.pdf create mode 100644 tests/plots/sbc_ranks.pdf create mode 100644 tests/plots/sbc_ranks_cdf.pdf diff --git a/src/metrics/coverage_fraction.py b/src/metrics/coverage_fraction.py index 85dba4a..9b3ff6c 100644 --- a/src/metrics/coverage_fraction.py +++ b/src/metrics/coverage_fraction.py @@ -1,6 +1,7 @@ import numpy as np -from typing import Any +from torch import tensor from tqdm import tqdm +from typing import Any from metrics.metric import Metric from utils.config import get_item @@ -53,15 +54,19 @@ def calculate(self): # find the percentile for the posterior for this observation # this is n_params dimensional # the units are in parameter space - confidence_lower = np.percentile( - samples.cpu(), - percentile_lower, - axis=0 + confidence_lower = tensor( + np.percentile( + samples.cpu(), + percentile_lower, + axis=0 + ) ) - confidence_upper = np.percentile( - samples.cpu(), - percentile_upper, - axis=0 + confidence_upper = tensor( + np.percentile( + samples.cpu(), + percentile_upper, + axis=0 + ) ) # this is asking if the true parameter value diff --git a/src/models/sbi_model.py b/src/models/sbi_model.py index 7e9b382..bcc3a0c 100644 --- a/src/models/sbi_model.py +++ b/src/models/sbi_model.py @@ -20,7 +20,7 @@ def sample_posterior(self, n_samples:int, y_true): # TODO typing (n_samples,), x=y_true, show_progress_bars=False - ).cpu() + ).cpu() # TODO Unbind from cpu def predict_posterior(self, data): posterior_samples = self.sample_posterior(data.y_true) diff --git a/src/plots/__init__.py b/src/plots/__init__.py index b8536cc..7ad7227 100644 --- a/src/plots/__init__.py +++ b/src/plots/__init__.py @@ -1,3 +1,9 @@ +from plots.cdf_ranks import CDFRanks +from plots.coverage_fraction import CoverageFraction +from plots.ranks import Ranks + Plots = { - + CDFRanks.__name__: CDFRanks, + CoverageFraction.__name__: CoverageFraction, + Ranks.__name__: Ranks } \ No newline at end of file diff --git a/src/plots/cdf_ranks.py b/src/plots/cdf_ranks.py new file mode 100644 index 0000000..03d5f66 --- /dev/null +++ b/src/plots/cdf_ranks.py @@ -0,0 +1,35 @@ +from sbi.analysis import sbc_rank_plot, run_sbc +from torch import tensor + +from plots.plot import Display +from utils.config import get_item + +class CDFRanks(Display): + def __init__(self, model, data, save: bool, show: bool, out_dir: str | None = None): + super().__init__(model, data, save, show, out_dir) + + def _plot_name(self): + return "cdf_ranks.png" + + def _data_setup(self): + thetas = tensor(self.data.theta_true()) + y_true = tensor(self.data.x_true()) + self.num_samples = get_item("metrics_common", "samples_per_inference", raise_exception=False) + + ranks, _ = run_sbc( + thetas, y_true, self.model.posterior, num_posterior_samples=self.num_samples + ) + self.ranks = ranks + + def _plot_settings(self): + self.colors = get_item("plots_common", "parameter_colors", raise_exception=False) + self.labels = get_item("plots_common", "parameter_labels", raise_exception=False) + + def _plot(self): + sbc_rank_plot( + self.ranks, + self.num_samples, + plot_type="cdf", + parameter_labels=self.labels, + colors=self.colors, + ) \ No newline at end of file diff --git a/src/plots/coverage_fraction.py b/src/plots/coverage_fraction.py new file mode 100644 index 0000000..c70d440 --- /dev/null +++ b/src/plots/coverage_fraction.py @@ -0,0 +1,78 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import colormaps as cm + +from metrics.coverage_fraction import CoverageFraction as coverage_fraction_metric +from plots.plot import Display +from utils.config import get_item + +class CoverageFraction(Display): + def __init__(self, model, data, save: bool, show: bool, out_dir: str | None = None): + super().__init__(model, data, save, show, out_dir) + + def _plot_name(self): + return "coverage_fraction.png" + + def _data_setup(self): + _, coverage = coverage_fraction_metric(self.model, self.data, out_dir=None).calculate() + self.coverage_fractions = coverage + + def _plot_settings(self): + self.labels = get_item("plots_common", "parameter_labels", raise_exception=False) + self.n_parameters = len(self.labels) + self.figure_size = tuple(get_item("plots_common", "figure_size", raise_exception=False)) + self.line_cycle = tuple(get_item("plots_common", "line_style_cycle", raise_exception=False)) + + def _plot( + self, + figure_alpha=1.0, + line_width=3, + legend_loc="lower right", + reference_line_label='Reference Line', + reference_line_style="k--", + x_label="Confidence Interval of the Posterior Volume", + y_label="Fraction of Lenses within Posterior Volume", + title="NPE" + ): + + n_steps = self.coverage_fractions.shape[0] + percentile_array = np.linspace(0, 1, n_steps) + color_cycler = iter(plt.cycler("color", cm.get_cmap(self.colorway).colors)) + line_style_cycler = iter(plt.cycler("line_style", self.line_cycle)) + + # Plotting + fig, ax = plt.subplots(1, 1, figsize=self.figure_size) + + # Iterate over the number of parameters in the model + for i in range(self.n_parameters): + + color = next(color_cycler)["color"] + line_style = next(line_style_cycler)["line_style"] + + ax.plot( + percentile_array, + self.coverage_fractions[:, i], + alpha=figure_alpha, + lw=line_width, + linestyle=line_style, + color=color, + label=self.labels[i], + ) + + ax.plot( + [0, 0.5, 1], [0, 0.5, 1], reference_line_style, lw=line_width, zorder=1000, + label=reference_line_label + ) + + ax.set_xlim([-0.05, 1.05]) + ax.set_ylim([-0.05, 1.05]) + + ax.text(0.03, 0.93, "Under-confident", horizontalalignment="left") + ax.text(0.3, 0.05, "Overconfident", horizontalalignment="left") + + ax.legend(loc=legend_loc) + + ax.set_xlabel(x_label) + ax.set_ylabel(y_label) + ax.set_title(title) + \ No newline at end of file diff --git a/src/plots/plot.py b/src/plots/plot.py index 3501cdf..2bbed32 100644 --- a/src/plots/plot.py +++ b/src/plots/plot.py @@ -3,21 +3,26 @@ import matplotlib.pyplot as plt from matplotlib import rcParams -from utils.config import get_section +from utils.config import get_item class Display: - def __init__(self, model, data, save:bool, show:bool, out_path:Optional[str]): + def __init__(self, model, data, save:bool, show:bool, out_dir:Optional[str]=None): self.save = save self.show = show - self.out_path = out_path.rstrip("/") - if self.save: - assert self.out_path is not None, "out_path required to save files." + self.data = data - if not os.path.exists(os.path.dirname(out_path)): - os.makedirs(os.path.dirname(out_path)) - + self.out_path = None + if (out_dir is None) and self.save: + self.out_path = get_item("common", "out_dir", raise_exception=False) + + elif self.save and (out_dir is not None): + self.out_path = out_dir + + if self.out_path is not None: + if not os.path.exists(os.path.dirname(self.out_path)): + os.makedirs(os.path.dirname(self.out_path)) + self.model = model - self._data_setup(data) self._common_settings() self._plot_settings() self.plot_name = self._plot_name() @@ -25,7 +30,7 @@ def __init__(self, model, data, save:bool, show:bool, out_path:Optional[str]): def _plot_name(self): raise NotImplementedError - def _data_setup(self, data): + def _data_setup(self): # Set all the vars used for the plot raise NotImplementedError @@ -38,25 +43,28 @@ def _plot(self, **kwrgs): raise NotImplementedError def _common_settings(self): - plot_common = get_section("plot_common", raise_exception=False) - rcParams["axes.spines.right"] = bool(plot_common['axis_spines']) - rcParams["axes.spines.top"] = bool(plot_common['axis_spines']) + + rcParams["axes.spines.right"] = bool(get_item('plots_common', 'axis_spines', raise_exception=False)) + rcParams["axes.spines.top"] = bool(get_item('plots_common','axis_spines', raise_exception=False)) # Style - self.colorway = plot_common["colorway"] - tight_layout = bool(plot_common['tight_layout']) + self.colorway = get_item('plots_common', "default_colorway", raise_exception=False) + tight_layout = bool(get_item('plots_common','tight_layout', raise_exception=False)) if tight_layout: plt.tight_layout() - plot_style = plot_common['plot_style'] + plot_style = get_item('plots_common','plot_style', raise_exception=False) plt.style.use(plot_style) def _finish(self): assert os.path.splitext(self.plot_name)[-1] != '', f"plot name, {self.plot_name}, is malformed. Please supply a name with an extension." if self.save: - plt.savefig(f"{self.out_path}/{self.plot_name}") - if self.plot: + plt.savefig(f"{self.out_path.rstrip('/')}/{self.plot_name}") + if self.show: plt.show() + + plt.cla() - def __call__(self, **kwargs) -> None: - self._plot(**kwargs) + def __call__(self, **plot_args) -> None: + self._data_setup() + self._plot(**plot_args) self._finish() \ No newline at end of file diff --git a/src/plots/ranks.py b/src/plots/ranks.py new file mode 100644 index 0000000..52581d7 --- /dev/null +++ b/src/plots/ranks.py @@ -0,0 +1,37 @@ +from sbi.analysis import sbc_rank_plot, run_sbc +from torch import tensor + +from plots.plot import Display +from utils.config import get_item + +class Ranks(Display): + def __init__(self, model, data, save: bool, show: bool, out_dir: str | None = None): + super().__init__(model, data, save, show, out_dir) + + def _plot_name(self): + return "ranks.png" + + def _data_setup(self): + thetas = tensor(self.data.theta_true()) + y_true = tensor(self.data.x_true()) + self.num_samples = get_item("metrics_common", "samples_per_inference", raise_exception=False) + + ranks, _ = run_sbc( + thetas, y_true, self.model.posterior, num_posterior_samples=self.num_samples + ) + self.ranks = ranks + + def _plot_settings(self): + self.colors = get_item("plots_common", "parameter_colors", raise_exception=False) + self.labels = get_item("plots_common", "parameter_labels", raise_exception=False) + + def _plot(self, num_bins=None): + sbc_rank_plot( + ranks=self.ranks, + num_posterior_samples=self.num_samples, + plot_type="hist", + num_bins=num_bins, + parameter_labels=self.labels, + colors=self.colors + ) + \ No newline at end of file diff --git a/src/utils/defaults.py b/src/utils/defaults.py index b2d75aa..0f511ac 100644 --- a/src/utils/defaults.py +++ b/src/utils/defaults.py @@ -13,17 +13,22 @@ "plots_common": { "axis_spines": False, "tight_layout": True, - "colorway": "virdids", - "plot_style": "fast" + "default_colorway": "viridis", + "plot_style": "fast", + "parameter_labels" : ['$m$','$b$'], + "parameter_colors": ['#9C92A3','#0F5257'], + "line_style_cycle": ["-", "-."], + "figure_size": [6, 6] }, "plots":{ - "type_of_plot":{"specific_kwargs"} + "CDFRanks":{}, + "Ranks":{"num_bins":None}, + "CoverageFraction":{} }, "metrics_common": { "use_progress_bar": False, "samples_per_inference":1000, "percentiles":[75, 85, 95] - }, "metrics":{ "AllSBC":{}, diff --git a/tests/conftest.py b/tests/conftest.py index 8d9f116..3be2a31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,11 +14,9 @@ def __init__(self): def __call__(self, thetas, samples): thetas = np.atleast_2d(thetas) - # Check if the input has the correct shape if thetas.shape[1] != 2: raise ValueError("Input tensor must have shape (n, 2) where n is the number of parameter sets.") - # Unpack the parameters if thetas.shape[0] == 1: # If there's only one set of parameters, extract them directly m, b = thetas[0, 0], thetas[0, 1] @@ -26,10 +24,7 @@ def __call__(self, thetas, samples): # If there are multiple sets of parameters, extract them for each row m, b = thetas[:, 0], thetas[:, 1] x = np.linspace(0, 100, samples) - rs = np.random.RandomState()#2147483648)# - # I'm thinking sigma could actually be a function of x - # if we want to get fancy down the road - # Generate random noise (epsilon) based on a normal distribution with mean 0 and standard deviation sigma + rs = np.random.RandomState() sigma = 1 epsilon = rs.normal(loc=0, scale=sigma, size=(len(x), thetas.shape[0])) @@ -77,7 +72,7 @@ def factory( plots=None, metrics=None ): - config = { "common": {}, "model": {}, "data":{}, "plot_common": {}, "plots":{}, "metric_common": {},"metrics":{}} + config = { "common": {}, "model": {}, "data":{}, "plots_common": {}, "plots":{}, "metrics_common": {},"metrics":{}} # Single settings if out_dir is not None: @@ -95,9 +90,11 @@ def factory( # Dict settings if plot_settings is not None: - config['plots_common'] = plot_settings + for key, item in plot_settings.items(): + config['plots_common'][key] = item if metrics_settings is not None: - config['metrics_common'] = metrics_settings + for key, item in metrics_settings.items(): + config['metrics_common'][key] = item if metrics is not None: if isinstance(metrics, dict): diff --git a/tests/plots/coverage.pdf b/tests/plots/coverage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bb08e02fe7128b31108c51915b4dbe33a1a3611d GIT binary patch literal 16720 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
  • (pTySFk$|MHMFUEs`|#pzfkpHi~B**1C)!O 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 WxjU8i*`Ux;NIXnXP(@D__P+r3#?d?g literal 0 HcmV?d00001 diff --git a/tests/plots/sbc_ranks_cdf.pdf b/tests/plots/sbc_ranks_cdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d4364964d8f6dc1e378cf1b789dabaeafa24734c GIT binary patch literal 36086 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 WiN5}%#}z>4Fa(&OpoXC)?0*63b+fPl literal 0 HcmV?d00001 diff --git a/tests/test_plots.py b/tests/test_plots.py index e69de29..e19bdc5 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -0,0 +1,56 @@ +import os +import pytest + +from utils.defaults import Defaults +from utils.config import Config, get_item +from plots import ( + Plots, + CDFRanks, + Ranks, + CoverageFraction +) + +@pytest.fixture +def plot_config(config_factory): + out_dir = "./temp_results/" + metrics_settings={"use_progress_bar":False, "samples_per_inference":10, "percentiles":[95]} + config = config_factory(out_dir=out_dir, metrics_settings=metrics_settings) + return config + +def test_all_plot_catalogued(): + '''Each metrics gets its own file, and each metric is included in the Metrics dictionary + so the client can use it. + This test verifies all metrics are cataloged''' + + all_files = os.listdir("src/plots/") + files_ignore = ['plot.py', '__init__.py', '__pycache__'] # All files not containing a metric + num_files = len([file for file in all_files if file not in files_ignore]) + assert len(Plots) == num_files + +def test_all_defaults(plot_config, mock_model, mock_data): + """ + Ensures each metric has a default set of parameters and is included in the defaults list + Ensures each test can initialize, regardless of the veracity of the output + """ + Config(plot_config) + for plot_name, plot_obj in Plots.items(): + assert plot_name in Defaults['plots'] + plot_obj(mock_model, mock_data, save=True, show=False) + +def test_plot_cdf(plot_config, mock_model, mock_data): + Config(plot_config) + plot = CDFRanks(mock_model, mock_data, save=True, show=False) + plot(**get_item("plots", "CDFRanks", raise_exception=False)) + assert os.path.exists(f"{plot.out_path}/{plot.plot_name}") + +def test_plot_ranks(plot_config, mock_model, mock_data): + Config(plot_config) + plot = Ranks(mock_model, mock_data, save=True, show=False) + plot(**get_item("plots", "Ranks", raise_exception=False)) + assert os.path.exists(f"{plot.out_path}/{plot.plot_name}") + +def test_plot_coverage(plot_config, mock_model, mock_data): + Config(plot_config) + plot = CoverageFraction(mock_model, mock_data, save=True, show=False) + plot(**get_item("plots", "CoverageFraction", raise_exception=False)) + assert os.path.exists(f"{plot.out_path}/{plot.plot_name}") \ No newline at end of file