From 5193b4363f9e07decc0c09a3c2c36b8a8a6e855a Mon Sep 17 00:00:00 2001 From: Isaac Gresham Date: Fri, 26 Sep 2025 15:43:54 +1000 Subject: [PATCH 1/9] Fix delta residual calculation --- refellips/objectiveSE.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/refellips/objectiveSE.py b/refellips/objectiveSE.py index d4a3f23..cbeb67e 100644 --- a/refellips/objectiveSE.py +++ b/refellips/objectiveSE.py @@ -178,9 +178,9 @@ def residuals(self, pvals=None): wavelength, aoi, psi_d, delta_d = self.data.data wavelength_aoi = np.c_[wavelength, aoi] - psi, delta = self.model(wavelength_aoi) - return np.r_[psi - psi_d, delta - delta_d] + delta_err = (delta - delta_d + 180) % 360 - 180 + return np.r_[psi - psi_d, delta_err] def chisqr(self, pvals=None): """ @@ -369,16 +369,16 @@ def logl(self, pvals=None): psi, delta = self.model(wavelength_aoi) - model = np.r_[psi, delta] - logl = 0.0 # TODO investigate ellipsometry uncertainties # here just set it to unity y_err = 1 if self.lnsigma is not None: + _model = np.r_[psi, delta] var_y = ( - y_err * y_err + np.exp(2 * float(self.lnsigma)) * model * model + y_err * y_err + + np.exp(2 * float(self.lnsigma)) * _model * _model ) else: var_y = y_err**2 @@ -387,7 +387,8 @@ def logl(self, pvals=None): if self.weighted: logl += np.log(2 * np.pi * var_y) - logl += (np.r_[psi_d, delta_d] - model) ** 2 / var_y + res = self.residuals(None) + logl += (res) ** 2 / var_y # nans play havoc if np.isnan(logl).any(): From 493c6c1ecf06b28f9f996bf7d4ac73bc07732d2f Mon Sep 17 00:00:00 2001 From: Isaac Gresham Date: Fri, 26 Sep 2025 15:48:10 +1000 Subject: [PATCH 2/9] Add new residuals test --- refellips/tests/test_refellips.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/refellips/tests/test_refellips.py b/refellips/tests/test_refellips.py index 981efda..7310b31 100644 --- a/refellips/tests/test_refellips.py +++ b/refellips/tests/test_refellips.py @@ -477,3 +477,42 @@ def test_TaucLorentz(): assert_allclose(psi, data.psi, rtol=0.0176) assert_allclose(delta, data.delta, rtol=0.0066) + + +def test_residuals(): + """ + Specifically testing for correct delta error handling (delta is an angle between 0 and 360) + """ + wavelength = np.linspace(400, 900, 100) + aoi = 70 + + si = load_material("silicon") + sio2 = load_material("silica") + polymer = Cauchy(A=1.43, B=0.0005) + solvent = Cauchy(A=1.45, B=0.0005) + + polymer_layer_1 = polymer(55) + polymer_layer_2 = polymer(45) + + struc_1 = solvent() | polymer_layer_1 | sio2(20) | si() + struc_2 = solvent() | polymer_layer_2 | sio2(20) | si() + + model_1 = ReflectModelSE(struc_1) + model_2 = ReflectModelSE(struc_2) + + psi_1, delta_1 = model_1(np.c_[wavelength, np.ones_like(wavelength) * aoi]) + psi_2, delta_2 = model_2(np.c_[wavelength, np.ones_like(wavelength) * aoi]) + faux_data = DataSE( + [wavelength, np.ones_like(wavelength) * aoi, psi_1, delta_1] + ) + + obj = ObjectiveSE(model=model_2, data=faux_data) + res = obj.residuals() + obj_psi_res = res[: int(len(res) / 2)] + obj_delta_res = res[int(len(res) / 2) :] + + test_psi_res = psi_2 - psi_1 + test_delta_res = (delta_2 - delta_1 + 180) % 360 - 180 + + assert_allclose(test_psi_res, obj_psi_res, rtol=0.002) + assert_allclose(test_delta_res, obj_delta_res, rtol=0.003) From 7fc8480a2ed177b2880b4f761e855f9f11d0911c Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Fri, 26 Sep 2025 16:05:46 +1000 Subject: [PATCH 3/9] STY --- refellips/dataSE.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refellips/dataSE.py b/refellips/dataSE.py index 6d89a35..b41c9bc 100644 --- a/refellips/dataSE.py +++ b/refellips/dataSE.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""" +""" " A basic representation of a 1D dataset """ From 886e1fe14af988b1ee046c87d9b3b74c3a72a9c6 Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Fri, 26 Sep 2025 16:13:36 +1000 Subject: [PATCH 4/9] STY --- refellips/dataSE.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refellips/dataSE.py b/refellips/dataSE.py index b41c9bc..6d4caa8 100644 --- a/refellips/dataSE.py +++ b/refellips/dataSE.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -""" " +""" A basic representation of a 1D dataset """ From b7b313ac66fae4746c0f2703ecafc15a15da1638 Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Fri, 26 Sep 2025 16:22:22 +1000 Subject: [PATCH 5/9] MAINT: circular distance --- refellips/objectiveSE.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/refellips/objectiveSE.py b/refellips/objectiveSE.py index cbeb67e..452c19b 100644 --- a/refellips/objectiveSE.py +++ b/refellips/objectiveSE.py @@ -18,6 +18,22 @@ from refnx._lib import flatten +def circular_distance(angle1, angle2, period=2*np.pi): + """ + Calculates the circular distance between two angles. + + Args: + angle1 (float or np.ndarray): The first angle(s) in radians. + angle2 (float or np.ndarray): The second angle(s) in radians. + period (float): The period of the circular domain (e.g., 2*np.pi for full circle). + + Returns: + float or np.ndarray: The shortest circular distance between the angles. + """ + diff = np.abs(angle1 - angle2) + return np.minimum(diff, period - diff) + + class ObjectiveSE(BaseObjective): """ Objective function for using with curvefitters such as @@ -179,7 +195,7 @@ def residuals(self, pvals=None): wavelength, aoi, psi_d, delta_d = self.data.data wavelength_aoi = np.c_[wavelength, aoi] psi, delta = self.model(wavelength_aoi) - delta_err = (delta - delta_d + 180) % 360 - 180 + delta_err = circular_distance(delta, delta_d, period=360) return np.r_[psi - psi_d, delta_err] def chisqr(self, pvals=None): From e8ca3ccd430d25a0864b95c621e7b0e87d7160c7 Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Fri, 26 Sep 2025 16:24:59 +1000 Subject: [PATCH 6/9] MAINT: circular distance --- refellips/tests/test_refellips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refellips/tests/test_refellips.py b/refellips/tests/test_refellips.py index 7310b31..9813867 100644 --- a/refellips/tests/test_refellips.py +++ b/refellips/tests/test_refellips.py @@ -512,7 +512,7 @@ def test_residuals(): obj_delta_res = res[int(len(res) / 2) :] test_psi_res = psi_2 - psi_1 - test_delta_res = (delta_2 - delta_1 + 180) % 360 - 180 + test_delta_res = np.abs((delta_2 - delta_1 + 180) % 360 - 180) assert_allclose(test_psi_res, obj_psi_res, rtol=0.002) assert_allclose(test_delta_res, obj_delta_res, rtol=0.003) From 29c70dc7273ab00317aca3d44430870c8a0374c9 Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Fri, 26 Sep 2025 16:25:18 +1000 Subject: [PATCH 7/9] STY --- refellips/objectiveSE.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refellips/objectiveSE.py b/refellips/objectiveSE.py index 452c19b..7d5420a 100644 --- a/refellips/objectiveSE.py +++ b/refellips/objectiveSE.py @@ -18,7 +18,7 @@ from refnx._lib import flatten -def circular_distance(angle1, angle2, period=2*np.pi): +def circular_distance(angle1, angle2, period=2 * np.pi): """ Calculates the circular distance between two angles. From c7413d77caea873697c159355dd9fcd357d6ba3b Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Fri, 26 Sep 2025 16:30:24 +1000 Subject: [PATCH 8/9] STY --- refellips/objectiveSE.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/refellips/objectiveSE.py b/refellips/objectiveSE.py index 7d5420a..8b26aee 100644 --- a/refellips/objectiveSE.py +++ b/refellips/objectiveSE.py @@ -22,10 +22,14 @@ def circular_distance(angle1, angle2, period=2 * np.pi): """ Calculates the circular distance between two angles. - Args: - angle1 (float or np.ndarray): The first angle(s) in radians. - angle2 (float or np.ndarray): The second angle(s) in radians. - period (float): The period of the circular domain (e.g., 2*np.pi for full circle). + Parameters + ---------- + angle1 : float, np.ndarray + First angle + angle2 : float, np.ndarray + Second angle + period : float + The period of the circular domain (e.g., 2*np.pi for full circle). Returns: float or np.ndarray: The shortest circular distance between the angles. From 30fa1ec170a4a50ade820c8a0a49adad183ddb69 Mon Sep 17 00:00:00 2001 From: Isaac Gresham Date: Fri, 26 Sep 2025 16:34:14 +1000 Subject: [PATCH 9/9] MAINT: Circular distance --- refellips/objectiveSE.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/refellips/objectiveSE.py b/refellips/objectiveSE.py index 8b26aee..ce76f7d 100644 --- a/refellips/objectiveSE.py +++ b/refellips/objectiveSE.py @@ -18,9 +18,11 @@ from refnx._lib import flatten -def circular_distance(angle1, angle2, period=2 * np.pi): +def circular_distance(angle1, angle2, period=360): """ Calculates the circular distance between two angles. + Units (rad or deg) do not matter as long as they are + consistent. Parameters ---------- @@ -29,7 +31,7 @@ def circular_distance(angle1, angle2, period=2 * np.pi): angle2 : float, np.ndarray Second angle period : float - The period of the circular domain (e.g., 2*np.pi for full circle). + The period of the circular domain (e.g.,360 for full circle). Returns: float or np.ndarray: The shortest circular distance between the angles.