inference-tools 0.13.2__py3-none-any.whl → 0.13.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
inference/mcmc/gibbs.py CHANGED
@@ -120,7 +120,7 @@ class Parameter:
120
120
  else:
121
121
  return self.upper - d % self.width
122
122
 
123
- def submit_accept_prob(self, p):
123
+ def submit_accept_prob(self, p: float):
124
124
  self.num += 1
125
125
  self.avg += p
126
126
  self.var += p * (1 - p)
@@ -375,23 +375,26 @@ class MetropolisChain(MarkovChain):
375
375
  ind = argmax(self.probs)
376
376
  return array([p.samples[ind] for p in self.params])
377
377
 
378
- def set_non_negative(self, parameter, flag=True):
378
+ def set_non_negative(self, parameter: int, flag=True):
379
379
  """
380
380
  Constrain a particular parameter to have non-negative values.
381
381
 
382
- :param int parameter: Index of the parameter which is to be set \
383
- as non-negative.
382
+ :param int parameter: \
383
+ Index of the parameter which is to be set as non-negative.
384
384
  """
385
385
  self.params[parameter].non_negative = flag
386
386
 
387
- def set_boundaries(self, parameter, boundaries, remove=False):
387
+ def set_boundaries(
388
+ self, parameter: int, boundaries: tuple[float, float], remove=False
389
+ ):
388
390
  """
389
391
  Constrain the value of a particular parameter to specified boundaries.
390
392
 
391
- :param int parameter: Index of the parameter for which boundaries \
392
- are to be set.
393
+ :param int parameter: \
394
+ Index of the parameter for which boundaries are to be set.
393
395
 
394
- :param boundaries: Tuple of boundaries in the format (lower_limit, upper_limit)
396
+ :param boundaries: \
397
+ Tuple of boundaries in the format (lower_limit, upper_limit)
395
398
  """
396
399
  if remove:
397
400
  self.params[parameter].remove_boundaries()
@@ -402,7 +405,7 @@ class MetropolisChain(MarkovChain):
402
405
  """
403
406
  Plot diagnostic traces that give information on how the chain is progressing.
404
407
 
405
- Currently this method plots:
408
+ Currently, this method plots:
406
409
 
407
410
  - The posterior log-probability as a function of step number, which is useful
408
411
  for checking if the chain has reached a maximum. Any early parts of the chain
inference/mcmc/hmc.py CHANGED
@@ -145,8 +145,8 @@ class HamiltonianChain(MarkovChain):
145
145
  else:
146
146
  raise ValueError(
147
147
  f"""\n
148
- [ HamiltonianChain error ]
149
- >> Failed to take step within maximum allowed attempts of {self.max_attempts}
148
+ \r[ HamiltonianChain error ]
149
+ \r>> Failed to take step within maximum allowed attempts of {self.max_attempts}
150
150
  """
151
151
  )
152
152
 
@@ -155,7 +155,9 @@ class HamiltonianChain(MarkovChain):
155
155
  self.leapfrog_steps.append(steps_taken)
156
156
  self.chain_length += 1
157
157
 
158
- def standard_leapfrog(self, t: ndarray, r: ndarray, n_steps: int):
158
+ def standard_leapfrog(
159
+ self, t: ndarray, r: ndarray, n_steps: int
160
+ ) -> tuple[ndarray, ndarray]:
159
161
  t_step = self.inv_mass * self.ES.epsilon
160
162
  r_step = self.inv_temp * self.ES.epsilon
161
163
  r += (0.5 * r_step) * self.grad(t)
@@ -166,7 +168,9 @@ class HamiltonianChain(MarkovChain):
166
168
  r += (0.5 * r_step) * self.grad(t)
167
169
  return t, r
168
170
 
169
- def bounded_leapfrog(self, t: ndarray, r: ndarray, n_steps: int):
171
+ def bounded_leapfrog(
172
+ self, t: ndarray, r: ndarray, n_steps: int
173
+ ) -> tuple[ndarray, ndarray]:
170
174
  t_step = self.inv_mass * self.ES.epsilon
171
175
  r_step = self.inv_temp * self.ES.epsilon
172
176
  r += (0.5 * r_step) * self.grad(t)
inference/mcmc/pca.py CHANGED
@@ -33,13 +33,13 @@ class PcaChain(MetropolisChain):
33
33
  and returns the posterior log-probability.
34
34
 
35
35
  :param start: \
36
- Vector of model parameters which correspond to the parameter-space coordinates
37
- at which the chain will start.
36
+ Values of the model parameters as a ``numpy.ndarray`` which correspond to the
37
+ parameter-space coordinates at which the chain will start.
38
38
 
39
39
  :param widths: \
40
- Vector of standard deviations which serve as initial guesses for the widths of
41
- the proposal distribution for each model parameter. If not specified, the starting
42
- widths will be approximated as 5% of the values in 'start'.
40
+ A ``numpy.ndarray`` of standard deviations which serve as initial guesses for
41
+ the widths of the proposal distribution for each model parameter. If not
42
+ specified, the starting widths will be approximated as 5% of the values in 'start'.
43
43
 
44
44
  :param bounds: \
45
45
  An instance of the ``inference.mcmc.Bounds`` class, or a sequence of two
@@ -103,26 +103,26 @@ class Bounds:
103
103
  if self.lower.ndim > 1 or self.upper.ndim > 1:
104
104
  raise ValueError(
105
105
  f"""\n
106
- [ {error_source} error ]
107
- >> Lower and upper bounds must be one-dimensional arrays, but
108
- >> instead have dimensions {self.lower.ndim} and {self.upper.ndim} respectively.
106
+ \r[ {error_source} error ]
107
+ \r>> Lower and upper bounds must be one-dimensional arrays, but
108
+ \r>> instead have dimensions {self.lower.ndim} and {self.upper.ndim} respectively.
109
109
  """
110
110
  )
111
111
 
112
112
  if self.lower.size != self.upper.size:
113
113
  raise ValueError(
114
114
  f"""\n
115
- [ {error_source} error ]
116
- >> Lower and upper bounds must be arrays of equal size, but
117
- >> instead have sizes {self.lower.size} and {self.upper.size} respectively.
115
+ \r[ {error_source} error ]
116
+ \r>> Lower and upper bounds must be arrays of equal size, but
117
+ \r>> instead have sizes {self.lower.size} and {self.upper.size} respectively.
118
118
  """
119
119
  )
120
120
 
121
121
  if (self.lower >= self.upper).any():
122
122
  raise ValueError(
123
123
  f"""\n
124
- [ {error_source} error ]
125
- >> All given upper bounds must be larger than the corresponding lower bounds.
124
+ \r[ {error_source} error ]
125
+ \r>> All given upper bounds must be larger than the corresponding lower bounds.
126
126
  """
127
127
  )
128
128
 
@@ -152,7 +152,7 @@ class Bounds:
152
152
  n = q % 2
153
153
  return self.lower + (1 - 2 * n) * rem + n * self.width
154
154
 
155
- def reflect_momenta(self, theta: ndarray):
155
+ def reflect_momenta(self, theta: ndarray) -> tuple[ndarray, ndarray]:
156
156
  q, rem = np_divmod(theta - self.lower, self.width)
157
157
  n = q % 2
158
158
  reflection = 1 - 2 * n
inference/pdf/kde.py CHANGED
@@ -42,8 +42,8 @@ class GaussianKDE(DensityEstimator):
42
42
  def __init__(
43
43
  self,
44
44
  sample: ndarray,
45
- bandwidth=None,
46
- cross_validation=False,
45
+ bandwidth: float = None,
46
+ cross_validation: bool = False,
47
47
  max_cv_samples=5000,
48
48
  ):
49
49
  self.sample = sort(array(sample).flatten()) # sorted array of the samples
@@ -136,7 +136,7 @@ class GaussianKDE(DensityEstimator):
136
136
  # A simple estimate which assumes the distribution close to a Gaussian
137
137
  return 1.06 * std(self.sample) / (self.sample.size**0.2)
138
138
 
139
- def cross_validation_bandwidth_estimator(self, initial_h):
139
+ def cross_validation_bandwidth_estimator(self, initial_h: float) -> float:
140
140
  """
141
141
  Selects the bandwidth by maximising a log-probability derived
142
142
  using a 'leave-one-out cross-validation' approach.
inference/pdf/unimodal.py CHANGED
@@ -4,6 +4,7 @@ from numpy import array, ndarray, linspace, zeros, atleast_1d
4
4
  from scipy.integrate import simpson, quad
5
5
  from scipy.optimize import minimize
6
6
  from inference.pdf.base import DensityEstimator
7
+ from inference.pdf.hdi import sample_hdi
7
8
 
8
9
 
9
10
  class UnimodalPdf(DensityEstimator):
@@ -21,9 +22,9 @@ class UnimodalPdf(DensityEstimator):
21
22
 
22
23
  def __init__(self, sample: ndarray):
23
24
  self.sample = array(sample).flatten()
24
- self.n_samps = sample.size
25
+ self.n_samps = self.sample.size
25
26
 
26
- # chebyshev quadtrature weights and axes
27
+ # chebyshev quadrature weights and axes
27
28
  self.sd = 0.2
28
29
  self.n_nodes = 128
29
30
  k = linspace(1, self.n_nodes, self.n_nodes)
@@ -34,27 +35,31 @@ class UnimodalPdf(DensityEstimator):
34
35
  # first minimise based on a slice of the sample, if it's large enough
35
36
  self.cutoff = 2000
36
37
  self.skip = max(self.n_samps // self.cutoff, 1)
37
-
38
- self.x = self.sample[:: self.skip]
39
- self.n = len(self.x)
38
+ self.fitted_samples = self.sample[:: self.skip]
40
39
 
41
40
  # makes guesses based on sample moments
42
- guesses = self.generate_guesses()
43
-
44
- # sort the guesses by the lowest score
45
- minfunc = lambda x: -self.posterior(x)
46
- guesses = sorted(guesses, key=minfunc)
41
+ guesses, self.bounds = self.generate_guesses_and_bounds()
42
+ # sort the guesses by the lowest cost
43
+ cost_func = lambda x: -self.posterior(x)
44
+ guesses = sorted(guesses, key=cost_func)
47
45
 
48
46
  # minimise based on the best guess
49
- self.min_result = minimize(minfunc, guesses[0], method="Nelder-Mead")
47
+ opt_method = "Nelder-Mead"
48
+ self.min_result = minimize(
49
+ fun=cost_func, x0=guesses[0], bounds=self.bounds, method=opt_method
50
+ )
50
51
  self.MAP = self.min_result.x
51
52
  self.mode = self.MAP[0]
52
53
 
53
54
  # if we were using a reduced sample, use full sample
54
55
  if self.skip > 1:
55
- self.x = self.sample
56
- self.n = self.n_samps
57
- self.min_result = minimize(minfunc, self.MAP, method="Nelder-Mead")
56
+ self.fitted_samples = self.sample
57
+ self.min_result = minimize(
58
+ fun=cost_func,
59
+ x0=self.MAP,
60
+ bounds=self.bounds,
61
+ method=opt_method,
62
+ )
58
63
  self.MAP = self.min_result.x
59
64
  self.mode = self.MAP[0]
60
65
 
@@ -66,25 +71,34 @@ class UnimodalPdf(DensityEstimator):
66
71
  self.upr_limit = x0 + s0 * (4 * exp(f) + 1)
67
72
  self.lwr_limit = x0 - s0 * (4 * exp(-f) + 1)
68
73
 
69
- def generate_guesses(self):
70
- mu, sigma, skew = self.sample_moments()
71
-
72
- x0 = [mu, mu - sigma * skew * 0.15, mu - sigma * skew * 0.3]
73
- v = [0, 5.0]
74
+ def generate_guesses_and_bounds(self) -> tuple[list, list]:
75
+ mu, sigma, skew = self.sample_moments(self.fitted_samples)
76
+ lwr, upr = sample_hdi(sample=self.sample, fraction=0.5)
77
+
78
+ bounds = [
79
+ (lwr, upr),
80
+ (sigma * 0.1, sigma * 10),
81
+ (0.0, 5.0),
82
+ (-3.0, 3.0),
83
+ (1e-2, 20.0),
84
+ (1.0, 6.0),
85
+ ]
86
+ x0 = [lwr * (1 - f) + upr * f for f in [0.3, 0.5, 0.7]]
74
87
  s0 = [sigma, sigma * 2]
88
+ ln_v = [0.25, 2.0]
75
89
  f = [0.5 * skew, skew]
76
90
  k = [1.0, 4.0, 8.0]
77
91
  q = [2.0]
78
92
 
79
- return [array(i) for i in product(x0, s0, v, f, k, q)]
93
+ return [array(i) for i in product(x0, s0, ln_v, f, k, q)], bounds
80
94
 
81
- def sample_moments(self):
82
- mu = mean(self.x)
83
- x2 = self.x**2
84
- x3 = x2 * self.x
95
+ @staticmethod
96
+ def sample_moments(samples: ndarray) -> tuple[float, float, float]:
97
+ mu = mean(samples)
98
+ x2 = samples**2
99
+ x3 = x2 * samples
85
100
  sig = sqrt(mean(x2) - mu**2)
86
101
  skew = (mean(x3) - 3 * mu * sig**2 - mu**3) / sig**3
87
-
88
102
  return mu, sig, skew
89
103
 
90
104
  def __call__(self, x: ndarray) -> ndarray:
@@ -112,35 +126,31 @@ class UnimodalPdf(DensityEstimator):
112
126
  integral = intervals.cumsum()[inverse_sort]
113
127
  return integral if x.size > 1 else integral[0]
114
128
 
115
- def posterior(self, paras):
116
- x0, s0, v, f, k, q = paras
129
+ def evaluate_model(self, x: ndarray, theta: ndarray) -> ndarray:
130
+ return self.pdf_model(x, theta) / self.norm(theta)
117
131
 
118
- # prior checks
119
- if (s0 > 0) & (0 < k < 20) & (1 < q < 6):
120
- normalisation = self.n * log(self.norm(paras))
121
- return self.log_pdf_model(self.x, paras).sum() - normalisation
122
- else:
123
- return -1e50
132
+ def posterior(self, theta: ndarray) -> float:
133
+ normalisation = self.fitted_samples.size * log(self.norm(theta))
134
+ return self.log_pdf_model(self.fitted_samples, theta).sum() - normalisation
124
135
 
125
- def norm(self, pvec):
126
- v = self.pdf_model(self.u, [0.0, self.sd, *pvec[2:]])
127
- integral = (self.w * v).sum() * pvec[1]
136
+ def norm(self, theta: ndarray) -> float:
137
+ v = self.pdf_model(self.u, [0.0, self.sd, *theta[2:]])
138
+ integral = (self.w * v).sum() * theta[1]
128
139
  return integral
129
140
 
130
- def pdf_model(self, x, pvec):
131
- return exp(self.log_pdf_model(x, pvec))
141
+ def pdf_model(self, x: ndarray, theta: ndarray) -> ndarray:
142
+ return exp(self.log_pdf_model(x, theta))
132
143
 
133
- def log_pdf_model(self, x, pvec):
134
- x0, s0, v, f, k, q = pvec
135
- v = exp(v) + 1
144
+ def log_pdf_model(self, x: ndarray, theta: ndarray) -> ndarray:
145
+ x0, s0, ln_v, f, k, q = theta
146
+ v = exp(ln_v)
136
147
  z0 = (x - x0) / s0
137
- ds = exp(f * tanh(z0 / k))
138
- z = z0 / ds
148
+ z = z0 * exp(-f * tanh(z0 / k))
139
149
 
140
150
  log_prob = -(0.5 * (1 + v)) * log(1 + (abs(z) ** q) / v)
141
151
  return log_prob
142
152
 
143
- def moments(self):
153
+ def moments(self) -> tuple[float, ...]:
144
154
  """
145
155
  Calculate the mean, variance skewness and excess kurtosis of the estimated PDF.
146
156
 
inference/posterior.py CHANGED
@@ -2,6 +2,8 @@
2
2
  .. moduleauthor:: Chris Bowman <chris.bowman.physics@gmail.com>
3
3
  """
4
4
 
5
+ from numpy import ndarray
6
+
5
7
 
6
8
  class Posterior:
7
9
  """
@@ -20,7 +22,7 @@ class Posterior:
20
22
  self.likelihood = likelihood
21
23
  self.prior = prior
22
24
 
23
- def __call__(self, theta):
25
+ def __call__(self, theta: ndarray) -> float:
24
26
  """
25
27
  Returns the log-posterior probability for the given set of model parameters.
26
28
 
@@ -32,7 +34,7 @@ class Posterior:
32
34
  """
33
35
  return self.likelihood(theta) + self.prior(theta)
34
36
 
35
- def gradient(self, theta):
37
+ def gradient(self, theta: ndarray) -> ndarray:
36
38
  """
37
39
  Returns the gradient of the log-posterior with respect to model parameters.
38
40
 
@@ -44,7 +46,7 @@ class Posterior:
44
46
  """
45
47
  return self.likelihood.gradient(theta) + self.prior.gradient(theta)
46
48
 
47
- def cost(self, theta):
49
+ def cost(self, theta: ndarray) -> float:
48
50
  """
49
51
  Returns the 'cost', defined as the negative log-posterior probability, for the
50
52
  given set of model parameters. Minimising the value of the cost therefore
@@ -58,7 +60,7 @@ class Posterior:
58
60
  """
59
61
  return -(self.likelihood(theta) + self.prior(theta))
60
62
 
61
- def cost_gradient(self, theta):
63
+ def cost_gradient(self, theta: ndarray) -> ndarray:
62
64
  """
63
65
  Returns the gradient of the negative log-posterior with respect to model parameters.
64
66