From 3aa6f8ac3b78aefe9b8498b7c1e526b18cfe414c Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:04:55 +0000 Subject: [PATCH 01/10] rename helper_functions.py to utils.py, fix a couple of formatting and a small numpy assignment deprecation --- flexknot/core.py | 2 +- flexknot/likelihoods.py | 6 ++---- flexknot/priors.py | 4 ++-- flexknot/{helper_functions.py => utils.py} | 0 tests/test_area.py | 2 +- tests/test_helper_functions.py | 7 ++++--- tests/test_likelihood.py | 2 +- tests/test_prior.py | 2 +- 8 files changed, 12 insertions(+), 13 deletions(-) rename flexknot/{helper_functions.py => utils.py} (100%) diff --git a/flexknot/core.py b/flexknot/core.py index 9611cd6..cc0be69 100644 --- a/flexknot/core.py +++ b/flexknot/core.py @@ -10,7 +10,7 @@ import numpy as np from scipy.integrate import quad -from flexknot.helper_functions import ( +from flexknot.utils import ( get_theta_n, get_x_nodes_from_theta, get_y_nodes_from_theta, diff --git a/flexknot/likelihoods.py b/flexknot/likelihoods.py index 0a5ea30..9f7a20f 100644 --- a/flexknot/likelihoods.py +++ b/flexknot/likelihoods.py @@ -4,10 +4,8 @@ import numpy as np from scipy.special import erf, logsumexp -from flexknot.helper_functions import ( - get_x_nodes_from_theta, - get_theta_n, -) +from flexknot.utils import get_x_nodes_from_theta + from flexknot.core import AdaptiveKnot, FlexKnot diff --git a/flexknot/priors.py b/flexknot/priors.py index 0fc6226..b1526ae 100644 --- a/flexknot/priors.py +++ b/flexknot/priors.py @@ -8,7 +8,7 @@ """ import numpy as np from pypolychord.priors import UniformPrior, SortedUniformPrior -from flexknot.helper_functions import ( +from flexknot.utils import ( create_theta, get_theta_n, get_x_nodes_from_theta, @@ -75,7 +75,7 @@ def __call__(self, hypercube): where Nmax is the greatest allowed value of floor(N). """ prior = np.empty(hypercube.shape) - prior[0] = self._N_prior(hypercube[0:1]) + prior[[0]] = self._N_prior(hypercube[0:1]) self.__n_x_nodes = int(prior[0]) prior[1:] = super().__call__(hypercube[1:]) return prior diff --git a/flexknot/helper_functions.py b/flexknot/utils.py similarity index 100% rename from flexknot/helper_functions.py rename to flexknot/utils.py diff --git a/tests/test_area.py b/tests/test_area.py index b6c22bf..f898a64 100644 --- a/tests/test_area.py +++ b/tests/test_area.py @@ -1,6 +1,6 @@ import numpy as np from flexknot import FlexKnot, AdaptiveKnot -from flexknot.helper_functions import get_theta_n +from flexknot.utils import get_theta_n def test_area(): diff --git a/tests/test_helper_functions.py b/tests/test_helper_functions.py index 5164364..5d7beec 100644 --- a/tests/test_helper_functions.py +++ b/tests/test_helper_functions.py @@ -1,9 +1,9 @@ """ -Test each of the functions from helper_functions.py +Test utils.py """ import numpy as np -from flexknot.helper_functions import ( +from flexknot.utils import ( create_theta, get_theta_n, get_x_nodes_from_theta, @@ -17,7 +17,8 @@ def test_get_theta_n(): """ - Check that get_theta_n() extracts the correct elements from [3, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6]. + Check that get_theta_n() extracts the correct elements from + [3, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6]. """ theta = np.array([5, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6]) diff --git a/tests/test_likelihood.py b/tests/test_likelihood.py index f37b8b8..d75830e 100644 --- a/tests/test_likelihood.py +++ b/tests/test_likelihood.py @@ -4,7 +4,7 @@ import numpy as np from scipy.special import erf from flexknot import FlexKnotLikelihood -from flexknot.helper_functions import create_theta +from flexknot.utils import create_theta def test_likelihood(): diff --git a/tests/test_prior.py b/tests/test_prior.py index 0c29513..9a475d6 100644 --- a/tests/test_prior.py +++ b/tests/test_prior.py @@ -4,7 +4,7 @@ import numpy as np from flexknot import AdaptiveKnotPrior, FlexKnotPrior -from flexknot.helper_functions import get_x_nodes_from_theta +from flexknot.utils import get_x_nodes_from_theta rng = np.random.default_rng() x_min = 0 From 8357cc90aae6d2ec75e1249cca252622f3004079 Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:07:30 +0000 Subject: [PATCH 02/10] use __all__ to avoid unused warning --- flexknot/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flexknot/__init__.py b/flexknot/__init__.py index ad1a4a7..c636a5b 100644 --- a/flexknot/__init__.py +++ b/flexknot/__init__.py @@ -10,3 +10,9 @@ from flexknot.core import AdaptiveKnot, FlexKnot from flexknot.likelihoods import FlexKnotLikelihood from flexknot.priors import AdaptiveKnotPrior, FlexKnotPrior + +__all__ = [ + "AdaptiveKnot", "FlexKnot", + "FlexKnotLikelihood", + "AdaptiveKnotPrior", "FlexKnotPrior", +] From d55a5687450103152076d39221e84afcf2def953 Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:25:12 +0000 Subject: [PATCH 03/10] docstring styling of __init__ and core --- flexknot/__init__.py | 2 +- flexknot/core.py | 85 +++++++++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/flexknot/__init__.py b/flexknot/__init__.py index c636a5b..288973b 100644 --- a/flexknot/__init__.py +++ b/flexknot/__init__.py @@ -1,5 +1,5 @@ """ -# Flex-Knot +# Flex-Knot. This repo contains flex-knots and associated likelihoods used in my toy_sine project, and likelihoods associated with them. diff --git a/flexknot/core.py b/flexknot/core.py index cc0be69..d85e3e5 100644 --- a/flexknot/core.py +++ b/flexknot/core.py @@ -1,12 +1,10 @@ """ -Linear INterpolation Functions. +Flex-knot. -theta refers to the full set of parameters for an adaptive linear interpolation model, -[n, y0, x1, y1, x2, y2, ..., x_n, y_n, y_n+1], -where n is the greatest allowed value of ceil(n). - -The reason for the interleaving of x and y is it avoids the need to know n. +x and y are interleaved so that N does not need to be provided for +a non-adaptive flex-knot. """ + import numpy as np from scipy.integrate import quad @@ -24,10 +22,14 @@ class FlexKnot: x_min: float x_max: float > x_min - Returns: - flexknot(x, theta) + Returns + ------- + flexknot(x, theta): callable + + theta has format + [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] + for N nodes. - theta in format [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] for N nodes. """ def __init__(self, x_min, x_max): @@ -36,14 +38,26 @@ def __init__(self, x_min, x_max): def __call__(self, x, theta): """ - Flex-knot with end nodes at x_min and x_max + Flex-knot with end nodes at x_min and x_max. + + For N nodes: + theta = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)]. + + y0 and y_(N-1) are the y values at x_min and x_max respecively. + + If theta only contains a single element, the flex-knot is constant. + If theta is empty, the flex-knot is constant at -1 (cosmology!) + + Parameters + ---------- + x : float or array-like - theta = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] for N nodes. + theta : array-like - y0 and y_(N-1) are the y values corresponding to x_min and x_max respecively. + Returns + ------- + float or array-like - If theta only contains a single element, the flex-knot is constant at that value. - If theta is empty, the flex-knot if constant at -1 (cosmology!) """ if 0 == len(theta): return np.full_like(x, -1) @@ -63,25 +77,31 @@ def __call__(self, x, theta): def area(self, theta0, theta1): """ - Calculate the area between the flex-knot with parameters - theta_0 and theta_1. + Area between two flex-knots. + + Parameters + ---------- + theta0 : array-like + + theta1 : array-like """ return quad(lambda x: np.abs(self(x, theta0)-self(x, theta1)), - self.x_min, self.x_max)[0] / (self.x_max - self.x_min) + self.x_min, self.x_max)[0] / (self.x_max - self.x_min) class AdaptiveKnot(FlexKnot): """ - Adaptive flex-knot which allows the number of parameters being used to vary. + Adaptive flex-knot which allows the number of knots to vary. x_min: float x_max: float > x_min - Returns: - adaptive_flexknot(x, theta) + Returns + ------- + adaptive_flexknot(x, theta): callable - The first element of theta is N; floor(N)-2 is number of interior nodes used in - the linear interpolation model. + The first element of theta is N; floor(N)-2 is number of interior nodes + used by the flexknot. theta = [N, y0, x1, y1, x2, y2, ..., x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)], where Nmax is the greatest allowed value of floor(N). @@ -92,14 +112,29 @@ class AdaptiveKnot(FlexKnot): def __call__(self, x, theta): """ - The first element of theta is N; floor(N)-2 is number of interior nodes used in - the linear interpolation model. This is then used to select the + Adaptive flex-knot with end nodes at x_min and x_max. + + The first element of theta is N; floor(N)-2 is number of interior nodes + used in the flexknot. This is then used to select the appropriate other elements of params to pass to flexknot() - theta = [N, y0, x1, y1, x2, y2, ..., x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)], + For a maximum of Nmax nodes: + theta = [N, y0, x1, y1, x2, y2, ..., + x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)], where Nmax is the greatest allowed value of floor(N). if floor(N) = 1, the flex-knot is constant at theta[-1] = y_(Nmax-1). if floor(N) = 0, the flex-knot is constant at -1 (cosmology!) + + Parameters + ---------- + x : float or array-like + + theta : array-like + + Returns + ------- + float or array-like + """ return super().__call__(x, get_theta_n(theta)) From 8809785a251bf8a0e35040695405b575ad235087 Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:36:35 +0000 Subject: [PATCH 04/10] linting likelihoods.py --- flexknot/likelihoods.py | 65 ++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/flexknot/likelihoods.py b/flexknot/likelihoods.py index 9f7a20f..3c09b47 100644 --- a/flexknot/likelihoods.py +++ b/flexknot/likelihoods.py @@ -1,6 +1,4 @@ -""" -Likelihoods using flex-knots. -""" +"""Likelihoods using flex-knots.""" import numpy as np from scipy.special import erf, logsumexp @@ -11,14 +9,16 @@ class FlexKnotLikelihood: """ - Likelihood for a flex-knot, relative to data described by xs, ys, and sigma. + Likelihood for a flex-knot, with data described by xs, ys, and sigma. - sigma is either sigma_y, [sigma_x, sigma_y], [sigma_ys] or [[sigma_xs], [sigma_ys]]. + sigma is either sigma_y, [sigma_x, sigma_y], [sigma_ys], + or [[sigma_xs], [sigma_ys]]. - (obviously the middle two are degenerate when len(sigma) = 2, in which case + (Obviously the middle two are degenerate when len(sigma) = 2, in which case [sigma_x, sigma_y] is assumed.) - Returns likelihood(theta) -> log(L), [] where [] is the (lack of) derived parameters. + Returns likelihood(theta) -> log(L), [] where [] is the (lack of) + derived parameters. """ def __init__(self, x_min, x_max, xs, ys, sigma, adaptive): @@ -28,28 +28,56 @@ def __init__(self, x_min, x_max, xs, ys, sigma, adaptive): def __call__(self, theta): """ - Likelihood relative to a flex-knot with parameters theta. + Likelihood of the data being described by flex-knot(theta). - If self.adaptive = True, the first element of theta is N; floor(N) is the number of - nodes used to calculate the likelihood. + If self.adaptive = True, the first element of theta is N; floor(N) is + the number of nodes in the flex-knot. - theta = [N, y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] for N nodes. + For N nodes, + theta = [N, y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)]. Otherwise, theta is the same but without N. theta = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)]. + + Parameters + ---------- + theta : array-like + + Returns + ------- + tuple(float, []) + """ return self._likelihood_function(theta) def create_likelihood_function(x_min, x_max, xs, ys, sigma, adaptive): """ - Creates a likelihood function for a flex-knot, relative to data descrived by xs, ys, and sigma. + Create a likelihood function for a flex-knot, for data xs, ys, sigma. - sigma is either sigma_y, [sigma_x, sigma_y], [sigma_ys] or [[sigma_xs], [sigma_ys]]. + sigma is either sigma_y, [sigma_x, sigma_y], [sigma_ys], + or [[sigma_xs], [sigma_ys]]. - (obviously the middle two are degenerate when len(sigma) = 2, in which case + (Obviously the middle two are degenerate when len(sigma) = 2, in which case [sigma_x, sigma_y] is assumed.) + + Returns likelihood(theta) -> log(L), [] where [] is the (lack of) + derived parameters. + + Parameters + ---------- + x_min : float + x_max : float > x_min + xs : array-like + ys : array-like + sigma : float or array-like + adaptive : bool + + Returns + ------- + likelihood(theta) -> log(L), [] + """ LOG_2_SQRT_2πλ = np.log(2) + 0.5 * np.log(2 * np.pi * (x_max - x_min)) @@ -77,7 +105,9 @@ def xy_errors_likelihood(theta): x_nodes = np.concatenate( ([x_min], get_x_nodes_from_theta(theta, adaptive), [x_max]) ) - # use flex-knots to get y nodes, as this is simplest way of dealing with N=0 or 1 + # use flex-knots to get y nodes, as this is + # the simplest way to deal with N=0 or 1 + y_nodes = flexknot(x_nodes, theta) ms = (y_nodes[1:] - y_nodes[:-1]) / (x_nodes[1:] - x_nodes[:-1]) @@ -90,8 +120,9 @@ def xy_errors_likelihood(theta): beta = (xs * var_y + (delta * ms).T * var_x).T / q gamma = (np.outer(xs, ms) - delta) ** 2 / 2 / q - t_minus = (np.sqrt(q / 2).T / (sigma_x * sigma_y)).T * (x_nodes[:-1] - beta) - t_plus = (np.sqrt(q / 2).T / (sigma_x * sigma_y)).T * (x_nodes[1:] - beta) + t = (np.sqrt(q / 2).T / (sigma_x * sigma_y)).T + t_minus = t * (x_nodes[:-1] - beta) + t_plus = t * (x_nodes[1:] - beta) logL = -len(xs) * LOG_2_SQRT_2πλ logL += np.sum( From d8a1ac07ccaf8d6df99544304d53614f2f463d9d Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:40:09 +0000 Subject: [PATCH 05/10] linting priors.py --- flexknot/priors.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/flexknot/priors.py b/flexknot/priors.py index b1526ae..f67c195 100644 --- a/flexknot/priors.py +++ b/flexknot/priors.py @@ -10,16 +10,13 @@ from pypolychord.priors import UniformPrior, SortedUniformPrior from flexknot.utils import ( create_theta, - get_theta_n, get_x_nodes_from_theta, get_y_nodes_from_theta, ) class FlexKnotPrior(UniformPrior): - """ - Interleaved uniform and sorted uniform priors appropriate for a flex-knot. - """ + """Interleaved uniform and sorted uniform priors for a flex-knot.""" def __init__(self, x_min, x_max, y_min, y_max): self._x_prior = SortedUniformPrior(x_min, x_max) @@ -29,10 +26,22 @@ def __call__(self, hypercube): """ Prior for flex-knot. - hypercube = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] for N nodes. + For N nodes: + hypercube -> [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)]. + + Parameters + ---------- + hypercube : array-like of Uniform(0, 1). + + Returns + ------- + theta : array-like of SortedUniform(x_min, x_max) + and Uniform(y_min, y_max). + """ if len(hypercube) > 2: - _x_prior = self._x_prior(get_x_nodes_from_theta(hypercube, adaptive=False)) + _x_prior = self._x_prior(get_x_nodes_from_theta(hypercube, + adaptive=False)) else: _x_prior = np.array([]) return create_theta( @@ -45,7 +54,8 @@ class AdaptiveKnotPrior(FlexKnotPrior): """ Interleaved uniform and sorted uniform priors appropriate for a flex-knot. - N_max: int is the maximum number of nodes to use with an interactive flex-knot. + N_max: int + The maximum number of nodes to use with an adaptive flex-knot. """ def __init__(self, x_min, x_max, y_min, y_max, N_min, N_max): @@ -60,7 +70,7 @@ def __init__(self, x_min, x_max, y_min, y_max, N_min, N_max): lambda hypercube_x: np.concatenate( ( self.__used_x_prior(hypercube_x[: self.__n_x_nodes]), - self.__unused_x_prior(hypercube_x[self.__n_x_nodes :]), + self.__unused_x_prior(hypercube_x[self.__n_x_nodes:]), ) ) if self.__n_x_nodes > 0 @@ -71,8 +81,11 @@ def __call__(self, hypercube): """ Prior for adaptive flex-knot. - hypercube = [N, y0, x1, y1, x2, y2, ..., x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)], - where Nmax is the greatest allowed value of floor(N). + hypercube = [N, y0, x1, y1, x2, y2, ..., + x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)], + where Nmax is the greatest allowed value of floor(N), i.e. + the maximum number of nodes. + """ prior = np.empty(hypercube.shape) prior[[0]] = self._N_prior(hypercube[0:1]) From 2fdbfe9bb23423bb564ce3acaaffb48a18e4acae Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:52:02 +0000 Subject: [PATCH 06/10] linting utils.py --- flexknot/utils.py | 116 +++++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/flexknot/utils.py b/flexknot/utils.py index 0ee5c98..81a703d 100644 --- a/flexknot/utils.py +++ b/flexknot/utils.py @@ -1,40 +1,54 @@ -""" -Helpful functions for dealing with the interleaved arrangement of theta. -""" +"""Utilities for dealing with the interleaved arrangement of theta.""" import numpy as np def validate_theta(theta, adaptive): - """ - Check that theta contains an odd/even number of elements for an adaptive/ - non-adaptive flex-knot, and in the adaptive case that floor(theta[0])-2 isn't - greater than the provided number of internal nodes. + """Check theta for mistakes. + + theta must contain an odd/even number of elements for an adaptive/ + non-adaptive flex-knot, + + In the adaptive case, floor(theta[0])-2 must not be greater than the + provided number of internal nodes. + + Parameters + ---------- + theta : array-like + + adaptive : bool + """ if adaptive: if len(theta) % 2 != 1: raise ValueError( - "theta must contain an odd number of elements for an adaptive flex-knot." + "theta must contain an odd number of elements " + "for an adaptive flex-knot." ) if np.floor(theta[0]) > (len(theta) + 1) / 2: - raise ValueError("n = ceil(theta[0]) exceeds the number of internal nodes.") + raise ValueError("n = ceil(theta[0]) exceeds the " + "number of internal nodes.") if not adaptive: if len(theta) > 1 and len(theta) % 2 != 0: - raise ValueError( - "theta must contain an even number of elements for a non-adaptive flex-knot." - ) + raise ValueError("theta must contain an even number of elements " + "for a non-adaptive flex-knot.") def get_theta_n(theta): - """ - Extracts the first n parameters from - - theta = [N, y0, x1, y1, x2, y2, ..., x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)] + """Extract the first N parameters from theta. + Parameters + ---------- + theta : array-like + [N, y0, x1, y1, x2, y2, ..., x_(Nmax-2), y_(Nmax-2), y_(Nmax-1)], where Nmax is the maximum value of floor(N). - returns theta_n = [y0, x1, y1, x2, y2, ..., x_floor(N)-2, y_floor(N)-2, y_(Nmax-1)] + Returns + ------- + theta_n : array-like + [y0, x1, y1, x2, y2, ..., x_floor(N)-2, y_floor(N)-2, y_(Nmax-1)] + """ validate_theta(theta, adaptive=True) @@ -44,7 +58,7 @@ def get_theta_n(theta): return np.array([]) theta_n = np.concatenate( ( - theta[1 : 2 * n + 2], # y0 and internal x and y + theta[1:2*n+2], # y0 and internal x and y theta[-1:], # y end node ) ) @@ -52,42 +66,78 @@ def get_theta_n(theta): def create_theta(x_nodes, y_nodes): - """ - Takes x_nodes = [x1, ... x_(N-2)] and y_nodes = [y0, y1, ..., y_(N-2), y_(N-1)] to - return theta = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)], where - N is the number of nodes. + """Interleave x and y nodes to create theta. + + Parameters + ---------- + x_nodes : array-like + [x1, ... x_(N-2)] + + y_nodes : array-like + [y0, y1, ..., y_(N-2), y_(N-1)] + + Returns + ------- + theta : array-like + [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] + """ if len(y_nodes) == 1: return y_nodes if len(x_nodes) + 2 != len(y_nodes): - raise ValueError("y_nodes must have exactly two more elements than x_nodes") + raise ValueError("y_nodes must have exactly two " + "more elements than x_nodes") n = len(x_nodes) theta = np.zeros(len(x_nodes) + len(y_nodes)) - theta[1 : 2 * n + 1 : 2] = x_nodes - theta[0 : 2 * n + 2 : 2] = y_nodes[:-1] + theta[1:2*n+1:2] = x_nodes + theta[0:2*n+2:2] = y_nodes[:-1] theta[-1] = y_nodes[-1] return theta def get_x_nodes_from_theta(theta, adaptive): - """ - Takes theta = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] to return - x_nodes = [x1, ... x_(N-2)], where N is the number of nodes. + """Get the x nodes from theta. + + Parameters + ---------- + theta : array-like + [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] + + adaptive : bool + Whether theta is for an adaptive or non-adaptive flex-knot. + + Returns + ------- + x_nodes : array-like + [x1, ... x_(N-2)] + """ validate_theta(theta, adaptive) if adaptive: theta = get_theta_n(theta) n = len(theta) // 2 - 1 - return theta[1 : 2 * n + 1 : 2] + return theta[1:2*n+1:2] def get_y_nodes_from_theta(theta, adaptive): - """ - Takes theta = [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] to return - y_nodes = [y0, y1, ..., y_(N-2), y_(N-1)], where N is the number of nodes. + """Get the y nodes from theta. + + Parameters + ---------- + theta : array-like + [y0, x1, y1, x2, y2, ..., x_(N-2), y_(N-2), y_(N-1)] + + adaptive : bool + Whether theta is for an adaptive or non-adaptive flex-knot. + + Returns + ------- + y_nodes : array-like + [y0, y1, ..., y_(N-2), y_(N-1)] + """ validate_theta(theta, adaptive) if adaptive: theta = get_theta_n(theta) n = len(theta) // 2 - 1 - return np.concatenate((theta[0 : 2 * n + 2 : 2], theta[-1:])) + return np.concatenate((theta[0:2*n+2:2], theta[-1:])) From b54972f3b40553eb8218b8daedce6c4da277315b Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:57:15 +0000 Subject: [PATCH 07/10] lint tests --- tests/test_flexknot.py | 9 +++++---- tests/test_likelihood.py | 28 +++++++++++++++++----------- tests/test_prior.py | 4 +++- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/test_flexknot.py b/tests/test_flexknot.py index a363d94..95986b0 100644 --- a/tests/test_flexknot.py +++ b/tests/test_flexknot.py @@ -18,8 +18,8 @@ def test_flexknot(): x_nodes = np.sort(rng.uniform(x_min, x_max, n)) y_nodes = rng.uniform(-10, 10, n + 2) theta = np.zeros(len(x_nodes) + len(y_nodes)) - theta[1 : 2 * n + 1 : 2] = x_nodes - theta[0 : 2 * n + 2 : 2] = y_nodes[:-1] + theta[1:2*n+1:2] = x_nodes + theta[0:2*n+2:2] = y_nodes[:-1] theta[-1] = y_nodes[-1] xs = np.linspace(x_min, x_max, 100) assert np.all( @@ -30,7 +30,7 @@ def test_flexknot(): def test_adaptive_flexknot(): """ - Test that AdaptiveKnot returns the same results as FlexKnot with appropriate arguments. + Test that AdaptiveKnot returns the same results as FlexKnot. """ x_min = 0 x_max = 6 @@ -38,7 +38,8 @@ def test_adaptive_flexknot(): theta_n = np.array([0, 1, 1, 2, 2, 3, 3, 6]) xs = np.linspace(x_min, x_max, 100) assert np.all( - FlexKnot(x_min, x_max)(xs, theta_n) == AdaptiveKnot(x_min, x_max)(xs, theta) + FlexKnot(x_min, x_max)(xs, theta_n) + == AdaptiveKnot(x_min, x_max)(xs, theta) ) diff --git a/tests/test_likelihood.py b/tests/test_likelihood.py index d75830e..6a18260 100644 --- a/tests/test_likelihood.py +++ b/tests/test_likelihood.py @@ -11,10 +11,11 @@ def test_likelihood(): """ Test the likelihood function against a trivial case with just sigma_y. - There are two datapoints at (0, 0) and (1, 1), and the interpolation function - is a single straight line between the same two points. Sigma is set to 1. + There are two datapoints at (0, 0) and (1, 1), and the flex-knot + is a single straight line between the same two points. sigma is set to 1. The value for the likelihood in this case should be -ln(2π). + """ x_min, x_max = 0, 1 x_nodes = np.array([]) @@ -25,19 +26,22 @@ def test_likelihood(): y_data = np.array([0, 1]) sigma = 1 - l = FlexKnotLikelihood(x_min, x_max, x_data, y_data, sigma, adaptive=False) - assert l(theta)[0] == -np.log(2 * np.pi) + logl = FlexKnotLikelihood(x_min, x_max, x_data, y_data, sigma, + adaptive=False) + assert logl(theta)[0] == -np.log(2 * np.pi) def test_likelihood_sigma_x(): """ - Test the likelihood function against a trivial case with sigma_x and sigma_y. + Test the likelihood function against a trivial case + with sigma_x and sigma_y. - There are two datapoints at (0, 0) and (1, 1), and the interpolation function - is a single straight line between the same two points. Sigma is set to 1 for - both x and y. + There are two datapoints at (0, 0) and (1, 1), and the flex-knot + is a single straight line between the same two points. Sigma is set to 1 + for both x and y. - The value for the likelihood in this case should be ln[(1/16π)(erf(1)-erf(0))(erf(0)-erf(-1))]. + The value for the likelihood in this case should be + ln[(1/16π)(erf(1)-erf(0))(erf(0)-erf(-1))]. """ x_min, x_max = 0, 1 @@ -49,7 +53,9 @@ def test_likelihood_sigma_x(): y_data = np.array([0, 1]) sigma = np.array([1, 1]) - l = FlexKnotLikelihood(x_min, x_max, x_data, y_data, sigma, adaptive=False) + logl = FlexKnotLikelihood(x_min, x_max, x_data, y_data, sigma, + adaptive=False) assert np.isclose( - l(theta)[0], np.log((erf(1) - erf(0)) * (erf(0) - erf(-1)) / (16 * np.pi)) + logl(theta)[0], np.log((erf(1) - erf(0)) * (erf(0) - erf(-1)) + / (16 * np.pi)) ) diff --git a/tests/test_prior.py b/tests/test_prior.py index 9a475d6..a3689b0 100644 --- a/tests/test_prior.py +++ b/tests/test_prior.py @@ -31,6 +31,8 @@ def test_adaptiveknotprior_x_nodes_are_sorted(): """ hypercube = rng.random(2 * N_max - 1) - prior = AdaptiveKnotPrior(x_min, x_max, y_min, y_max, N_min, N_max)(hypercube) + prior = AdaptiveKnotPrior( + x_min, x_max, y_min, y_max, N_min, N_max + )(hypercube) assert np.all(np.diff(get_x_nodes_from_theta(prior, adaptive=True)) >= 0) From aa55f90699c92c69900c83154084ba2b2b93ea49 Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 13:58:13 +0000 Subject: [PATCH 08/10] replace black with pytest, flake8, pydocstyle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4c0990a..fe9afd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,5 +10,5 @@ dependencies = ["numpy", "scipy", "pypolychord"] repository = "https://github.com/Ormorod/flexknot" [project.optional-dependencies] -dev = ["black"] +dev = ["pytest", "flake8", "pydocstyle"] From 0d6ef45ca67b6f9fed48983182ca6300337aa8f2 Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 14:00:04 +0000 Subject: [PATCH 09/10] add pythons 3.11 and 3.12 to matrix --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b4a090f..7c17cea 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 From 6250471413e362a16831d685e6b854d57d754f87 Mon Sep 17 00:00:00 2001 From: AdamOrmondroyd Date: Thu, 25 Jan 2024 14:03:22 +0000 Subject: [PATCH 10/10] update checkout and setup-python versions --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 7c17cea..41e71af 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,9 +19,9 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies (Linux)