From a65702d4dd33f6555e3f53643f01fe605686c5ae Mon Sep 17 00:00:00 2001 From: drewoldag <47493171+drewoldag@users.noreply.github.com> Date: Tue, 25 Oct 2022 09:08:44 -0700 Subject: [PATCH] Separated the metric implementation and Ensemble wrapper functions. Updated tests as needed. (#119) --- src/qp/metrics.py | 269 -------------------------------- src/qp/metrics/__init__.py | 3 + src/qp/metrics/array_metrics.py | 118 ++++++++++++++ src/qp/metrics/metrics.py | 189 ++++++++++++++++++++++ tests/qp/test_metrics.py | 94 ++++++----- 5 files changed, 366 insertions(+), 307 deletions(-) delete mode 100644 src/qp/metrics.py create mode 100644 src/qp/metrics/__init__.py create mode 100644 src/qp/metrics/array_metrics.py create mode 100644 src/qp/metrics/metrics.py diff --git a/src/qp/metrics.py b/src/qp/metrics.py deleted file mode 100644 index 4e2af9a3..00000000 --- a/src/qp/metrics.py +++ /dev/null @@ -1,269 +0,0 @@ -"""This module implements some performance metrics for distribution parameterization""" - -import numpy as np - -from scipy.integrate import quad -from scipy.optimize import minimize_scalar - -from qp.utils import safelog, epsilon - -def calculate_moment(p, N, limits, dx=0.01): - """ - Calculates a moment of a qp.PDF object - - Parameters - ---------- - p: qp.PDF object - the PDF whose moment will be calculated - N: int - order of the moment to be calculated - limits: tuple of floats - endpoints of integration interval over which to calculate moments - dx: float - resolution of integration grid - vb: Boolean - print progress to stdout? - - Returns - ------- - M: float - value of the moment - """ - # Make a grid from the limits and resolution - d = int((limits[-1] - limits[0]) / dx) - grid = np.linspace(limits[0], limits[1], d) - dx = (limits[-1] - limits[0]) / (d - 1) - # Evaluate the functions on the grid - pe = p.gridded(grid)[1] - # calculate the moment - grid_to_N = grid ** N - M = quick_moment(pe, grid_to_N, dx) - return M - -def quick_moment(p_eval, grid_to_N, dx): - """ - Calculates a moment of an evaluated PDF - - Parameters - ---------- - p_eval: numpy.ndarray, float - the values of a probability distribution - grid: numpy.ndarray, float - the grid upon which p_eval was evaluated - dx: float - the difference between regular grid points - N: int - order of the moment to be calculated - - Returns - ------- - M: float - value of the moment - """ - M = np.dot(p_eval, grid_to_N) * dx - return M - -def calculate_kld(p, q, limits, dx=0.01): - """ - Calculates the Kullback-Leibler Divergence between two qp.PDF objects. - - Parameters - ---------- - p: PDF object - probability distribution whose distance _from_ `q` will be calculated. - q: PDF object - probability distribution whose distance _to_ `p` will be calculated. - limits: tuple of floats - endpoints of integration interval in which to calculate KLD - dx: float - resolution of integration grid - - Returns - ------- - Dpq: float - the value of the Kullback-Leibler Divergence from `q` to `p` - - Notes - ----- - TO DO: change this to calculate_kld - TO DO: have this take number of points not dx! - """ - if p.shape != q.shape: - raise ValueError('Cannot calculate KLD between two ensembles with different shapes') - - # Make a grid from the limits and resolution - N = int((limits[-1] - limits[0]) / dx) - grid = np.linspace(limits[0], limits[1], N) - dx = (limits[-1] - limits[0]) / (N - 1) - # Evaluate the functions on the grid and normalize - pe = p.gridded(grid) - pn = pe[1] - qe = q.gridded(grid) - qn = qe[1] - # Normalize the evaluations, so that the integrals can be done - # (very approximately!) by simple summation: - # pn = pe / np.sum(pe) - #denominator = max(np.sum(qe), epsilon) - # qn = qe / np.sum(qe)#denominator - # Compute the log of the normalized PDFs - # logquotient = safelog(pn / qn) - # logp = safelog(pn) - # logq = safelog(qn) - # Calculate the KLD from q to p - Dpq = quick_kld(pn, qn, dx=dx)# np.dot(pn * logquotient, np.ones(len(grid)) * dx) - if np.any(Dpq < 0.): #pragma: no cover - print('broken KLD: '+str((Dpq, pn, qn, dx))) - Dpq = epsilon*np.ones(Dpq.shape) - return Dpq - -def quick_kld(p_eval, q_eval, dx=0.01): - """ - Calculates the Kullback-Leibler Divergence between two evaluations of PDFs. - - Parameters - ---------- - p_eval: numpy.ndarray, float - evaluations of probability distribution whose distance _from_ `q` will be calculated - q_eval: numpy.ndarray, float - evaluations of probability distribution whose distance _to_ `p` will be calculated. - dx: float - resolution of integration grid - - Returns - ------- - Dpq: float - the value of the Kullback-Leibler Divergence from `q` to `p` - - Notes - ----- - TO DO: change this to quick_kld - """ - logquotient = safelog(p_eval) - safelog(q_eval) - # logp = safelog(pn) - # logq = safelog(qn) - # Calculate the KLD from q to p - Dpq = dx * np.sum(p_eval * logquotient, axis=-1) - return Dpq - -def calculate_rmse(p, q, limits, dx=0.01): - """ - Calculates the Root Mean Square Error between two qp.PDF objects. - - Parameters - ---------- - p: PDF object - probability distribution function whose distance between its truth and the approximation of `q` will be calculated. - q: PDF object - probability distribution function whose distance between its approximation and the truth of `p` will be calculated. - limits: tuple of floats - endpoints of integration interval in which to calculate RMS - dx: float - resolution of integration grid - - Returns - ------- - rms: float - the value of the RMS error between `q` and `p` - - Notes - ----- - TO DO: change dx to N - """ - if p.shape != q.shape: - raise ValueError('Cannot calculate RMSE between two ensembles with different shapes') - - # Make a grid from the limits and resolution - N = int((limits[-1] - limits[0]) / dx) - grid = np.linspace(limits[0], limits[1], N) - dx = (limits[-1] - limits[0]) / (N - 1) - # Evaluate the functions on the grid - pe = p.gridded(grid)[1] - qe = q.gridded(grid)[1] - # Calculate the RMS between p and q - rms = quick_rmse(pe, qe, N)# np.sqrt(dx * np.sum((pe - qe) ** 2)) - return rms - -def quick_rmse(p_eval, q_eval, N): - """ - Calculates the Root Mean Square Error between two evaluations of PDFs. - - Parameters - ---------- - p_eval: numpy.ndarray, float - evaluation of probability distribution function whose distance between - its truth and the approximation of `q` will be calculated. - q_eval: numpy.ndarray, float - evaluation of probability distribution function whose distance between - its approximation and the truth of `p` will be calculated. - N: int - number of points at which PDFs were evaluated - - Returns - ------- - rms: float - the value of the RMS error between `q` and `p` - """ - # Calculate the RMS between p and q - rms = np.sqrt(np.sum((p_eval - q_eval) ** 2, axis=-1) / N) - return rms - -def risk_based_point_estimate(p, limits=(np.inf, np.inf)): - """ - Calculates the risk based point estimates of a qp.Ensemble object. - Algorithm as defined in 4.2 of 'Photometric redshifts for Hyper Suprime-Cam - Subaru Strategic Program Data Release 1' (Tanaka et al. 2018). - - Parameters - ---------- - p: qp.Ensemble object - Ensemble of PDFs to be evalutated - limits, tuple of floats - The limits at which to evaluate possible z_best estimates. - If custom limits are not provided then all potential z value will be - considered using the scipy.optimize.minimize_scalar function. - - Returns - ------- - rbpes: array of floats - The risk based point estimates of the provided ensemble. - """ - rbpes = [] - for n in range(0, p.npdf): - rbpes.append(quick_rbpe(p[n], limits)) - - return np.array(rbpes) - -def quick_rbpe(p_eval, limits=(np.inf, np.inf)): - """ - Calculates the risk based point estimate of a qp.Ensemble object with npdf == 1. - - Parameters - ---------- - p_eval: qp.Ensemble object - Ensemble of a single PDF to be evaluated. - limits, tuple of floats - The limits at which to evaluate possible z_best estimates. - If custom limits are not provided then all potential z value will be - considered using the scipy.optimize.minimize_scalar function. - - Returns - ------- - rbpe: float - The risk based point estimate of the provided ensemble. - """ - if p_eval.npdf != 1: - raise ValueError('quick_rbpe only handles Ensembles with a single PDF, for ensembles with more than one PDF, use the qp.metrics.risk_based_point_estimate function.') - - loss = lambda x : 1. - (1. / (1. + (pow((x / .15), 2)))) - pz = lambda z: p_eval.pdf(z)[0][0] - lower = p_eval.ppf(0.01)[0][0] - upper = p_eval.ppf(0.99)[0][0] - - def find_z_risk(zp): - integrand = lambda z : pz(z) * loss((zp - z) / (1. + z)) - return quad(integrand, lower, upper)[0] - - if limits[0] == np.inf: - return minimize_scalar(find_z_risk).x - return minimize_scalar(find_z_risk, bounds=(limits[0], limits[1]), method='bounded').x - diff --git a/src/qp/metrics/__init__.py b/src/qp/metrics/__init__.py new file mode 100644 index 00000000..b29814ec --- /dev/null +++ b/src/qp/metrics/__init__.py @@ -0,0 +1,3 @@ +from .array_metrics import * +from .metrics import * +from .metrics import _calculate_grid_parameters # added for testing purposes diff --git a/src/qp/metrics/array_metrics.py b/src/qp/metrics/array_metrics.py new file mode 100644 index 00000000..76774f95 --- /dev/null +++ b/src/qp/metrics/array_metrics.py @@ -0,0 +1,118 @@ +"""This module implements metric calculations that are independent of qp.Ensembles""" + +import numpy as np +from scipy.integrate import quad +from scipy.optimize import minimize_scalar + +from qp.utils import safelog + + +def quick_moment(p_eval, grid_to_N, dx): + """ + Calculates a moment of an evaluated PDF + + Parameters + ---------- + p_eval: numpy.ndarray, float + the values of a probability distribution + grid: numpy.ndarray, float + the grid upon which p_eval was evaluated + dx: float + the difference between regular grid points + N: int + order of the moment to be calculated + + Returns + ------- + M: float + value of the moment + """ + M = np.dot(p_eval, grid_to_N) * dx + return M + +def quick_kld(p_eval, q_eval, dx=0.01): + """ + Calculates the Kullback-Leibler Divergence between two evaluations of PDFs. + + Parameters + ---------- + p_eval: numpy.ndarray, float + evaluations of probability distribution whose distance _from_ `q` will be calculated + q_eval: numpy.ndarray, float + evaluations of probability distribution whose distance _to_ `p` will be calculated. + dx: float + resolution of integration grid + + Returns + ------- + Dpq: float + the value of the Kullback-Leibler Divergence from `q` to `p` + """ + + # safelog would be easy to isolate if array_metrics is ever extracted + logquotient = safelog(p_eval) - safelog(q_eval) + + # Calculate the KLD from q to p + Dpq = dx * np.sum(p_eval * logquotient, axis=-1) + return Dpq + +def quick_rmse(p_eval, q_eval, N): + """ + Calculates the Root Mean Square Error between two evaluations of PDFs. + + Parameters + ---------- + p_eval: numpy.ndarray, float + evaluation of probability distribution function whose distance between + its truth and the approximation of `q` will be calculated. + q_eval: numpy.ndarray, float + evaluation of probability distribution function whose distance between + its approximation and the truth of `p` will be calculated. + N: int + number of points at which PDFs were evaluated + + Returns + ------- + rms: float + the value of the RMS error between `q` and `p` + """ + # Calculate the RMS between p and q + rms = np.sqrt(np.sum((p_eval - q_eval) ** 2, axis=-1) / N) + return rms + +def quick_rbpe(pdf_function, integration_bounds, limits=(np.inf, np.inf)): + """ + Calculates the risk based point estimate of a qp.Ensemble object with npdf == 1. + + Parameters + ---------- + pdf_function, python function + The function should calculate the value of a pdf at a given x value + integration_bounds, 2-tuple of floats + The integration bounds - typically (ppf(0.01), ppf(0.99)) for the given distribution + limits, tuple of floats + The limits at which to evaluate possible z_best estimates. + If custom limits are not provided then all potential z value will be + considered using the scipy.optimize.minimize_scalar function. + + Returns + ------- + rbpe: float + The risk based point estimate of the provided ensemble. + """ + + def calculate_loss(x): + return 1. - (1. / (1. + (pow((x / .15), 2)))) + + lower = integration_bounds[0] + upper = integration_bounds[1] + + def find_z_risk(zp): + def integrand(z): + return pdf_function(z) * calculate_loss((zp - z) / (1. + z)) + + return quad(integrand, lower, upper)[0] + + if limits[0] == np.inf: + return minimize_scalar(find_z_risk).x + return minimize_scalar(find_z_risk, bounds=(limits[0], limits[1]), method='bounded').x diff --git a/src/qp/metrics/metrics.py b/src/qp/metrics/metrics.py new file mode 100644 index 00000000..74696951 --- /dev/null +++ b/src/qp/metrics/metrics.py @@ -0,0 +1,189 @@ +"""This module implements some performance metrics for distribution parameterization""" + +from collections import namedtuple +from functools import partial + +import numpy as np + +import qp.metrics.array_metrics as array_metrics +from qp.utils import epsilon + +Grid = namedtuple('Grid', ['grid_values', 'cardinality', 'resolution', 'limits']) + +def _calculate_grid_parameters(limits, dx:float=0.01) -> Grid: + """ + Create a grid of points and return parameters describing it. + + Args: + limits (Iterable) often a 2-tuple or numpy array with shape (2,) + the max and min values of the 1d grid + dx (float, optional): + the desired delta between points. Used to define the cardinality. Defaults to 0.01. + + Returns: + Grid: a namedtuple containing a 1d grid's values and attributes. + grid_values: np.array with size = cardinality + cardinality: int, number of elements in grid_value + resolution: float, equal to grid_values[i] - grid_values[i-1] + limits: 2-tuple, the limits passed in and used in this function + """ + cardinality = int((limits[-1] - limits[0]) / dx) + grid_values = np.linspace(limits[0], limits[1], cardinality) + resolution = (limits[-1] - limits[0]) / (cardinality - 1) + + return Grid(grid_values, cardinality, resolution, limits) + +def calculate_moment(p, N, limits, dx=0.01): + """ + Calculates a moment of a qp.Ensemble object + + Parameters + ---------- + p: qp.Ensemble object + the collection of PDFs whose moment will be calculated + N: int + order of the moment to be calculated + limits: tuple of floats + endpoints of integration interval over which to calculate moments + dx: float + resolution of integration grid + + Returns + ------- + M: float + value of the moment + """ + # Make a grid from the limits and resolution + grid = _calculate_grid_parameters(limits, dx) + + # Evaluate the functions on the grid + pe = p.gridded(grid.grid_values)[1] + + # calculate the moment + grid_to_N = grid.grid_values ** N + M = array_metrics.quick_moment(pe, grid_to_N, grid.resolution) + + return M + + +def calculate_kld(p, q, limits, dx=0.01): + """ + Calculates the Kullback-Leibler Divergence between two qp.Ensemble objects. + + Parameters + ---------- + p: Ensemble object + probability distribution whose distance _from_ `q` will be calculated. + q: Ensemble object + probability distribution whose distance _to_ `p` will be calculated. + limits: tuple of floats + endpoints of integration interval in which to calculate KLD + dx: float + resolution of integration grid + + Returns + ------- + Dpq: float + the value of the Kullback-Leibler Divergence from `q` to `p` + + Notes + ----- + TO DO: have this take number of points not dx! + """ + if p.shape != q.shape: + raise ValueError('Cannot calculate KLD between two ensembles with different shapes') + + # Make a grid from the limits and resolution + grid = _calculate_grid_parameters(limits, dx) + + # Evaluate the functions on the grid and normalize + pe = p.gridded(grid.grid_values) + pn = pe[1] + qe = q.gridded(grid.grid_values) + qn = qe[1] + + # Calculate the KLD from q to p + Dpq = array_metrics.quick_kld(pn, qn, grid.resolution)# np.dot(pn * logquotient, np.ones(len(grid)) * dx) + + if np.any(Dpq < 0.): #pragma: no cover + print('broken KLD: '+str((Dpq, pn, qn, grid.resolution))) + Dpq = epsilon*np.ones(Dpq.shape) + return Dpq + + +def calculate_rmse(p, q, limits, dx=0.01): + """ + Calculates the Root Mean Square Error between two qp.Ensemble objects. + + Parameters + ---------- + p: qp.Ensemble object + probability distribution function whose distance between its truth and the approximation of `q` will be calculated. + q: qp.Ensemble object + probability distribution function whose distance between its approximation and the truth of `p` will be calculated. + limits: tuple of floats + endpoints of integration interval in which to calculate RMS + dx: float + resolution of integration grid + + Returns + ------- + rms: float + the value of the RMS error between `q` and `p` + + Notes + ----- + TO DO: change dx to N + """ + if p.shape != q.shape: + raise ValueError('Cannot calculate RMSE between two ensembles with different shapes') + + # Make a grid from the limits and resolution + grid = _calculate_grid_parameters(limits, dx) + + # Evaluate the functions on the grid + pe = p.gridded(grid.grid_values)[1] + qe = q.gridded(grid.grid_values)[1] + + # Calculate the RMS between p and q + rms = array_metrics.quick_rmse(pe, qe, grid.cardinality)# np.sqrt(dx * np.sum((pe - qe) ** 2)) + + return rms + + +def calculate_rbpe(p, limits=(np.inf, np.inf)): + """ + Calculates the risk based point estimates of a qp.Ensemble object. + Algorithm as defined in 4.2 of 'Photometric redshifts for Hyper Suprime-Cam + Subaru Strategic Program Data Release 1' (Tanaka et al. 2018). + + Parameters + ---------- + p: qp.Ensemble object + Ensemble of PDFs to be evalutated + limits, tuple of floats + The limits at which to evaluate possible z_best estimates. + If custom limits are not provided then all potential z value will be + considered using the scipy.optimize.minimize_scalar function. + + Returns + ------- + rbpes: array of floats + The risk based point estimates of the provided ensemble. + """ + rbpes = [] + + def evaluate_pdf_at_z(z, dist): + return dist.pdf(z)[0][0] + + for n in range(0, p.npdf): + + if p[n].npdf != 1: + raise ValueError('quick_rbpe only handles Ensembles with a single PDF, for ensembles with more than one PDF, use the qp.metrics.risk_based_point_estimate function.') + + this_dist_pdf_at_z = partial(evaluate_pdf_at_z, dist=p[n]) + integration_bounds = (p[n].ppf(0.01)[0][0], p[n].ppf(0.99)[0][0]) + + rbpes.append(array_metrics.quick_rbpe(this_dist_pdf_at_z, integration_bounds, limits)) + + return np.array(rbpes) diff --git a/tests/qp/test_metrics.py b/tests/qp/test_metrics.py index 60f2bc4c..ccd77e4f 100644 --- a/tests/qp/test_metrics.py +++ b/tests/qp/test_metrics.py @@ -4,6 +4,7 @@ import unittest import qp +import qp.metrics import numpy as np from qp import test_funcs @@ -19,10 +20,11 @@ def setUp(self): """ self.ens_n = test_funcs.build_ensemble(qp.stats.norm_gen.test_data['norm']) #pylint: disable=no-member self.ens_n_shift = test_funcs.build_ensemble(qp.stats.norm_gen.test_data['norm_shifted']) #pylint: disable=no-member + self.ens_n_multi = test_funcs.build_ensemble(qp.stats.norm_gen.test_data['norm_multi_d']) #pylint: disable=no-member locs = 2* (np.random.uniform(size=(10,1))-0.5) scales = 1 + 0.2*(np.random.uniform(size=(10,1))-0.5) - self.ens_n_plus_one = qp.Ensemble(qp.stats.norm, data=dict(loc=locs, scale=scales)) #pylint: disable=no-member + self.ens_n_plus_one = qp.Ensemble(qp.stats.norm, data=dict(loc=locs, scale=scales)) #pylint: disable=no-member bins = np.linspace(-5, 5, 11) self.ens_s = self.ens_n.convert_to(qp.spline_gen, xvals=bins, method="xy") @@ -30,6 +32,16 @@ def setUp(self): def tearDown(self): """ Clean up any mock data files created by the tests. """ + def test_calculate_grid_parameters(self): + limits = (0,1) + dx = 1./11 + grid_params = qp.metrics._calculate_grid_parameters(limits, dx) #pylint: disable=W0212 + assert grid_params.cardinality == 11 + assert grid_params.resolution == 0.1 + assert grid_params.grid_values[0] == limits[0] + assert grid_params.grid_values[-1] == limits[-1] + assert grid_params.grid_values.size == grid_params.cardinality + def test_kld(self): """ Test the calculate_kld method """ kld = qp.metrics.calculate_kld(self.ens_n, self.ens_n_shift, limits=(0.,2.5)) @@ -119,18 +131,18 @@ def test_rmse_different_shapes(self): self.assertTrue('Cannot calculate RMSE between two ensembles with different shapes' in str(context.exception)) - def test_rpbe(self): + def test_rbpe(self): """ Test the risk_based_point_estimate method """ - rpbe = qp.metrics.risk_based_point_estimate(self.ens_n, limits=(0.,2.5)) - assert np.all(rpbe >= 0.) - assert np.all(rpbe <= 2.5) + rbpe = qp.metrics.calculate_rbpe(self.ens_n, limits=(0.,2.5)) + assert np.all(rbpe >= 0.) + assert np.all(rbpe <= 2.5) - def test_rpbe_no_limits(self): + def test_rbpe_no_limits(self): """ Test the risk_based_point_estimate method when the user doesn't provide a set of limits """ - rpbe = qp.metrics.risk_based_point_estimate(self.ens_n) - assert np.all(rpbe >= -2.) + rbpe = qp.metrics.calculate_rbpe(self.ens_n) + assert np.all(rbpe >= -2.) - def test_rpbe_alternative_ensembles(self): + def test_rbpe_alternative_ensembles(self): """ Test the risk_based_point_estimate method against different types of ensembles """ bins = np.linspace(-5, 5, 11) quants = np.linspace(0.01, 0.99, 7) @@ -141,42 +153,48 @@ def test_rpbe_alternative_ensembles(self): ens_q = self.ens_n[0].convert_to(qp.quant_piecewise_gen, quants=quants) ens_m = self.ens_n[0].convert_to(qp.mixmod_gen, samples=1000, ncomps=3) - rpbe_histogram = qp.metrics.risk_based_point_estimate(ens_h, limits=(0.,2.5)) - assert np.all(rpbe_histogram >= 0.) - assert np.all(rpbe_histogram <= 2.5) + rbpe_histogram = qp.metrics.calculate_rbpe(ens_h, limits=(0.,2.5)) + assert np.all(rbpe_histogram >= 0.) + assert np.all(rbpe_histogram <= 2.5) - rpbe_interp = qp.metrics.risk_based_point_estimate(ens_i, limits=(0.,2.5)) - assert np.all(rpbe_interp >= 0.) - assert np.all(rpbe_interp <= 2.5) + rbpe_interp = qp.metrics.calculate_rbpe(ens_i, limits=(0.,2.5)) + assert np.all(rbpe_interp >= 0.) + assert np.all(rbpe_interp <= 2.5) - rpbe_spline = qp.metrics.risk_based_point_estimate(ens_s, limits=(0.,2.5)) - assert np.all(rpbe_spline >= 0.) - assert np.all(rpbe_spline <= 2.5) + rbpe_spline = qp.metrics.calculate_rbpe(ens_s, limits=(0.,2.5)) + assert np.all(rbpe_spline >= 0.) + assert np.all(rbpe_spline <= 2.5) - rpbe_quants = qp.metrics.risk_based_point_estimate(ens_q, limits=(0.,2.5)) - assert np.all(rpbe_quants >= 0.) - assert np.all(rpbe_quants <= 2.5) + rbpe_quants = qp.metrics.calculate_rbpe(ens_q, limits=(0.,2.5)) + assert np.all(rbpe_quants >= 0.) + assert np.all(rbpe_quants <= 2.5) - rpbe_mixmod = qp.metrics.risk_based_point_estimate(ens_m, limits=(0.,2.5)) - assert np.all(rpbe_mixmod >= 0.) - assert np.all(rpbe_mixmod <= 2.5) + rbpe_mixmod = qp.metrics.calculate_rbpe(ens_m, limits=(0.,2.5)) + assert np.all(rbpe_mixmod >= 0.) + assert np.all(rbpe_mixmod <= 2.5) - def test_quick_rpbe(self): - """ Test the quick_rpbe method """ - rpbe = qp.metrics.quick_rbpe(self.ens_n[0], limits=(0.,2.5)) - assert np.all(rpbe >= 0.) - assert np.all(rpbe <= 2.5) - - def test_quick_rpbe_no_limits(self): - """ Test the quick_rpbe method """ - rpbe = qp.metrics.quick_rbpe(self.ens_n[0]) - assert np.all(rpbe >= -2.) - - def test_quick_rpbe_multiple_pdfs(self): - """ Ensure that the quick_rpbe function fails when trying to evaluate an ensemble with more than one pdf. """ + def test_quick_rbpe(self): + """ Test the quick_rbpe method """ + def eval_pdf_at_z(z): + return self.ens_n[0].pdf(z)[0][0] + integration_bounds = (self.ens_n[0].ppf(0.01)[0][0], self.ens_n[0].ppf(0.99)[0][0]) + rbpe = qp.metrics.quick_rbpe(eval_pdf_at_z, integration_bounds, limits=(0.,2.5)) + assert np.all(rbpe >= 0.) + assert np.all(rbpe <= 2.5) + + def test_quick_rbpe_no_limits(self): + """ Test the quick_rbpe method """ + def eval_pdf_at_z(z): + return self.ens_n[0].pdf(z)[0][0] + integration_bounds = (self.ens_n[0].ppf(0.01)[0][0], self.ens_n[0].ppf(0.99)[0][0]) + rbpe = qp.metrics.quick_rbpe(eval_pdf_at_z, integration_bounds) + assert np.all(rbpe >= -2.) + + def test_rbpe_multiple_pdfs(self): + """ Ensure that calculate_rbpe function fails when working with multi-dimensional Ensembles. """ with self.assertRaises(ValueError) as context: - qp.metrics.quick_rbpe(self.ens_n, limits=(0.,2.5)) + _ = qp.metrics.calculate_rbpe(self.ens_n_multi, limits=(0.,2.5)) error_msg = 'quick_rbpe only handles Ensembles with a single PDF' self.assertTrue(error_msg in str(context.exception))