Skip to content

Commit 0dcf00d

Browse files
authored
Merge pull request #10 from comet-toolkit/fix-postive-semi-definite
fixes for semi-positive corr and general throughout
2 parents 8edb99a + 7fa0760 commit 0dcf00d

10 files changed

Lines changed: 83 additions & 31 deletions

curepy/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@
3535
# Utilities
3636
from curepy.utilities.plotting import plot_corner
3737
from curepy.utilities.maths import lnlike
38-
from curepy.utilities.distributions import ln_uniform, ln_normal, ln_multi_normal, ln_trunc_normal
38+
from curepy.utilities.distributions import (
39+
ln_uniform,
40+
ln_normal,
41+
ln_multi_normal,
42+
ln_trunc_normal,
43+
)
3944
from curepy.utilities.utilities import flatten_array, reshape_array, format_correlation
4045

4146
from ._version import get_versions

curepy/container/measurement.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(
1414
u_y_rand: Optional[np.ndarray] = None,
1515
u_y_syst: Optional[np.ndarray] = None,
1616
corr_y: Optional[Union[str, np.ndarray]] = None,
17+
skip_invcov: bool = False,
1718
) -> None:
1819
"""
1920
Container class for measurement variable data.
@@ -29,6 +30,7 @@ def __init__(
2930
Accepted values: ``None``, ``"rand"`` (random), ``"syst"``
3031
(systematic), or a square matrix whose side length equals the
3132
length of ``y``.
33+
:param skip_invcov: If ``True``, skip the computation of the inverse covariance matrix (which is only needed for certain retrieval methods like optimal estimation).
3234
"""
3335

3436
u_y_total, corr_y = self._format_uncertainty(
@@ -43,9 +45,13 @@ def __init__(
4345

4446
self.corr_y = util.format_correlation(self.y_flat, corr_y)
4547

48+
self.corr_y, self.cholesky, self.W = self.return_corr_cholesky_whitening(
49+
self.corr_y
50+
)
51+
4652
self._check_shapes(self.y_flat, self.u_y_flat, self.corr_y)
4753

48-
if corr_y is not None:
54+
if corr_y is not None and not skip_invcov:
4955
self.invcov = self.calculate_inv_cov(self.u_y_flat, self.corr_y)
5056
else:
5157
self.invcov = None
@@ -136,6 +142,30 @@ def _format_uncertainty(u_total, u_rand, u_syst, corr):
136142
tot_corr = cm.convert_cov_to_corr(tot_cov, tot)
137143
return tot, tot_corr
138144

145+
@staticmethod
146+
def return_corr_cholesky_whitening(corr: Optional[np.ndarray]) -> tuple:
147+
"""
148+
Return the correlation matrix, its Cholesky decomposition, and the whitening matrix.
149+
150+
:param corr: Correlation matrix, or ``None``.
151+
:returns: Tuple of ``(corr, cholesky, W)`` where ``cholesky`` is
152+
the Cholesky decomposition of the correlation matrix, or
153+
``None`` if ``corr`` is ``None``, and ``W`` is the whitening matrix.
154+
"""
155+
if corr is not None:
156+
try:
157+
cholesky = np.linalg.cholesky(corr)
158+
W = np.linalg.solve(cholesky, np.eye(cholesky.shape[0]))
159+
return corr, cholesky, W
160+
except np.linalg.LinAlgError:
161+
# If the correlation matrix is not positive definite, use the nearest positive definite matrix
162+
corr_pd = cm.nearestPD_cholesky(corr, return_cholesky=False, corr=True)
163+
cholesky = np.linalg.cholesky(corr_pd)
164+
W = np.linalg.solve(cholesky, np.eye(cholesky.shape[0]))
165+
return corr_pd, cholesky, W
166+
else:
167+
return None, None, None
168+
139169
@staticmethod
140170
def calculate_inv_cov(unc: np.ndarray, corr: np.ndarray) -> np.ndarray:
141171
"""
@@ -151,5 +181,4 @@ def calculate_inv_cov(unc: np.ndarray, corr: np.ndarray) -> np.ndarray:
151181
if np.array_equal(cov, np.diag(np.diag(cov))):
152182
return np.diag(1 / np.diag(cov))
153183
else:
154-
# might need a check for PD here
155184
return np.linalg.inv(cov)

curepy/container/tests/test_measurement.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def test_calculate_inv_cov(self, mock_convert_corr_to_cov, mock_inv):
4040
def test_init_format_correlation_called(
4141
self, mock_format, mock_check, mock_convert
4242
):
43+
# Configure mock to return a valid correlation matrix
44+
mock_format.return_value = np.eye(len(y))
4345

4446
meas = Measurement(y, u_y, corr_y="rand")
4547

curepy/retrieval_methods/base.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,35 +127,44 @@ def find_chisum(
127127
).flatten()
128128
)
129129
diff = modelled_data - self.retrieval_input.measurement_obj.y_flat
130+
131+
# Only normalize by u_y_flat if it's available
132+
if self.retrieval_input.measurement_obj.u_y_flat is not None:
133+
diff_norm = diff / self.retrieval_input.measurement_obj.u_y_flat
134+
else:
135+
diff_norm = diff
136+
130137
if np.isfinite(np.sum(diff)):
131-
if self.retrieval_input.measurement_obj.invcov is None:
132-
return np.sum(
133-
(diff) ** 2 / self.retrieval_input.measurement_obj.u_y_flat**2
134-
)
138+
if self.retrieval_input.measurement_obj.cholesky is None:
139+
chisq = np.sum(
140+
(diff_norm) ** 2
141+
) # this is equivalent to using an identity matrix for the inverse covariance, which is appropriate when only uncorrelated uncertainties are available
142+
135143
else:
136144
if len(repeat_dims) == 0:
137-
return np.dot(
138-
np.dot(diff.T, self.retrieval_input.measurement_obj.invcov),
139-
diff,
140-
)
145+
y = self.retrieval_input.measurement_obj.W @ diff_norm
146+
chisq = y.T @ y
141147
elif len(repeat_dims) == 1:
142148
sum = 0
143149
for i in range(diff.shape[repeat_dims[0]]):
144-
diffi = np.take(diff, i, repeat_dims[0])
145-
sum += np.dot(
146-
np.dot(
147-
diffi.T, self.retrieval_input.measurement_obj.invcov
148-
),
149-
diffi,
150-
)
151-
return sum
150+
diff_norm_i = np.take(diff_norm, i, repeat_dims[0])
151+
y = self.retrieval_input.measurement_obj.W @ diff_norm_i
152+
sum += y.T @ y
153+
chisq = sum
152154
else:
153155
raise ValueError(
154156
"Methods for multiple repeat dimensions are not yet implemented,"
155157
)
156158
else:
157159
print("The difference between model and observations is infinite")
158-
return np.inf
160+
chisq = np.inf
161+
162+
if chisq < 0:
163+
raise ValueError(
164+
"The chi-squared cost is negative, which should not be possible. Check the inputs and the measurement function for errors."
165+
)
166+
167+
return chisq
159168

160169
def lnprob(self, theta: np.ndarray) -> float:
161170
"""

curepy/retrieval_methods/optimal_estimation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def _run_retrieval(
5656
self.retrieval_input.measurement_function_obj.initial_guess
5757
)
5858

59-
res = minimize(-self.lnprob, theta_0)
59+
res = minimize(lambda theta: -self.lnprob(theta), theta_0)
6060

6161
if self.Jx is None:
6262
Jx = self.calculate_Jx(res.x)

curepy/retrieval_methods/tests/test_base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ def make_mock_retrieval_input_for_chisum(
1414
invcov=None,
1515
u_y=None,
1616
b=None,
17+
L=None,
18+
W=None,
1719
):
1820
retrieval_input = RetrievalInput()
1921
# measurement function object
@@ -27,6 +29,9 @@ def make_mock_retrieval_input_for_chisum(
2729
retrieval_input.measurement_obj.y_flat = np.array(y_flat)
2830
retrieval_input.measurement_obj.invcov = invcov
2931
retrieval_input.measurement_obj.u_y_flat = u_y
32+
retrieval_input.measurement_obj.cholesky = L
33+
retrieval_input.measurement_obj.W = W
34+
3035
# ancillary
3136
retrieval_input.ancillary_obj = MagicMock()
3237
retrieval_input.ancillary_obj.b = b
@@ -137,7 +142,7 @@ def test_multiple_repeat_dims_raises_error(self):
137142
retrieval_input = make_mock_retrieval_input_for_chisum(
138143
measurement_function_output=np.array([1.0, 2.0]),
139144
y_flat=np.array([1.0, 1.0]),
140-
invcov=np.eye(2),
145+
L=np.eye(2),
141146
u_y=None,
142147
b=None,
143148
)
@@ -181,6 +186,8 @@ def test_chisum_with_invcov_no_repeat(self):
181186
invcov=invcov,
182187
u_y=None,
183188
b=None,
189+
L=np.eye(2),
190+
W=np.eye(2),
184191
)
185192

186193
dr = DummyRetrieval()

examples/multidimensional_MCMC_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def quadratic(a, b, c, x, d):
2525
y = data + noise
2626

2727
meas_func = MeasurementFunction(quadratic, [0.5, 0.2, -10])
28-
meas = Measurement(y, noise, "rand")
28+
meas = Measurement(y, noise, corr_y="rand")
2929
ancill = AncillaryParameter([x, d], [None, 1], [None, None], b_MC_steps=3)
3030

3131
inputs = RetrievalInput(meas_func, meas, ancill)
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
MeasurementFunction,
33
Measurement,
44
AncillaryParameter,
5-
LPU,
5+
OE,
66
RetrievalInput,
77
)
88

@@ -24,7 +24,7 @@ def quadratic(a, b, c, x, d):
2424
y = data + noise
2525

2626
meas_func = MeasurementFunction(quadratic, [0.5, 0.2, -10])
27-
meas = Measurement(y, noise, "rand")
27+
meas = Measurement(y, noise, corr_y="rand")
2828
ancill = AncillaryParameter(
2929
[x, d],
3030
[0.01 * np.ones_like(x), 1],
@@ -41,7 +41,7 @@ def quadratic(a, b, c, x, d):
4141

4242
inputs = RetrievalInput(meas_func, meas, ancill)
4343

44-
ret = LPU()
44+
ret = OE()
4545

4646
results = ret.run_retrieval(inputs)
4747

examples/simple_quadratic_MCMC_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def quadratic(a, b, c, x, d):
2828
y = data + noise
2929

3030
meas_func = MeasurementFunction(quadratic, [0.5, 0.2, -10])
31-
meas = Measurement(y, noise, np.eye(len(x)))
31+
meas = Measurement(y, noise, corr_y=np.eye(len(x)))
3232
ancill = AncillaryParameter([x, d], [None, 1], [np.eye(len(x)), None], b_MC_steps=3)
3333
prior = Prior(
3434
["normal"] * 3,
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
MeasurementFunction,
33
Measurement,
44
AncillaryParameter,
5-
LPU,
5+
OE,
66
RetrievalInput,
77
Prior,
88
)
@@ -26,7 +26,7 @@ def quadratic(a, b, c, x, d):
2626
y = data + noise
2727

2828
meas_func = MeasurementFunction(quadratic, [0.5, 0.2, -10])
29-
meas = Measurement(y, noise, np.eye(len(x)))
29+
meas = Measurement(y, noise, corr_y=np.eye(len(x)))
3030
ancill = AncillaryParameter([x, d], [None, 0.05], [None, None])
3131
prior = Prior(
3232
["normal"] * 3,
@@ -36,12 +36,12 @@ def quadratic(a, b, c, x, d):
3636

3737
inputs = RetrievalInput(meas_func, meas, ancill, prior)
3838

39-
ret = LPU()
39+
ret = OE()
4040

4141
results = ret.run_retrieval(inputs)
4242

4343
print(results.values)
4444
print(results.uncertainties)
4545
plt.plot(x, quadratic(*results.values, x, d))
4646
plt.scatter(x, y, alpha=0.5, c="orange")
47-
plt.savefig(os.path.join(example_dir, "LPU_test.png"))
47+
plt.savefig(os.path.join(example_dir, "OE_test.png"))

0 commit comments

Comments
 (0)