From f2a98bb5bd28c34d5d1c00bfaac7f6a5438a3103 Mon Sep 17 00:00:00 2001 From: Fabian Joswig Date: Sun, 17 Nov 2024 12:18:49 +0100 Subject: [PATCH 1/3] [Feat] Meas class for gaussian error propagation. --- pyerrors/obs.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- tests/obs_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pyerrors/obs.py b/pyerrors/obs.py index a1c2fd55..14a5a245 100644 --- a/pyerrors/obs.py +++ b/pyerrors/obs.py @@ -9,6 +9,8 @@ from scipy.stats import skew, skewtest, kurtosis, kurtosistest import numdifftools as nd from itertools import groupby +from typing import Optional, Union +import uuid from .covobs import Covobs # Improve print output of numpy.ndarrays containing Obs objects. @@ -1354,6 +1356,9 @@ def __init__(self, N): final_result[i_val]._value = new_val final_result[i_val].reweighted = reweighted + if not final_result[i_val].idl: + final_result[i_val].gm() + if multi == 0: final_result = final_result.item() @@ -1810,7 +1815,7 @@ def covobs_to_obs(co): co : Covobs Covobs to be embedded into the Obs """ - o = Obs([], [], means=[]) + o = Obs(samples=[], names=[], means=[]) o._value = co.value o.names.append(co.name) o._covobs[co.name] = co @@ -1822,7 +1827,7 @@ def covobs_to_obs(co): means = [means] for i in range(len(means)): - ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) + ol.append(covobs_to_obs(Covobs(float(means[i]), cov, name, pos=i, grad=grad))) if ol[0].covobs[name].N != len(means): raise Exception('You have to provide %d mean values!' % (ol[0].N)) if len(ol) == 1: @@ -1830,6 +1835,43 @@ def covobs_to_obs(co): return ol +class Meas(Obs): + """Class for a scalar measurement. + + Convenience wrapper for scalar measurements. + """ + + def __init__(self, value: Union[float, int], dvalue: Union[float, int], name: Optional[str] = None): + """ Initialize Meas object. + + Parameters + ---------- + value : float + Mean value of the measurement. + dvalue : list or array + Standard error of the measurement. + name : Optional[str] + Optional name identifier for the measurement. If none is specified, a random uuid + string is used instead. + """ + if not isinstance(value, (float, int)): + raise TypeError(f"value has to be a flaot or int, not {type(value)}") + if not isinstance(dvalue, (float, int)): + raise TypeError(f"dvalue has to be a float or int, not {type(dvalue)}") + super().__init__(samples=[], names=[], means=[]) + if name is None: + name = uuid.uuid4().hex + else: + if not isinstance(name, str): + raise TypeError(f"name has to be a str, not {type(name)}") + + co = Covobs(float(value), float(dvalue) ** 2, name) + self._value = co.value + self.names.append(co.name) + self._covobs[co.name] = co + self._dvalue = np.sqrt(co.errsq()) + + def _determine_gap(o, e_content, e_name): gaps = [] for r_name in e_content[e_name]: diff --git a/tests/obs_test.py b/tests/obs_test.py index 726ecffa..aa9e5bb2 100644 --- a/tests/obs_test.py +++ b/tests/obs_test.py @@ -1458,3 +1458,29 @@ def test_missing_replica(): for op in [[O1O2, O1O2b], [O1O2O3, O1O2O3b]]: assert np.isclose(op[1].value, op[0].value) assert np.isclose(op[1].dvalue, op[0].dvalue, atol=0, rtol=5e-2) + + +def test_meas(): + meas1 = pe.Meas(1.0, 0.1) + meas2 = pe.Meas(2, 1) + + assert meas1 + meas2 == meas2 + meas1 + assert meas1 - meas2 == -(meas2 - meas1) + assert meas1 * meas2 == meas2 * meas1 + + identical_sum = meas1 + meas1 + assert identical_sum == 2 * meas1 + assert np.isclose(identical_sum.dvalue, 2 * meas1.dvalue) + + meas1_new = pe.Meas(1.0, 0.1) + not_identical_sum = meas1 + meas1_new + assert not_identical_sum.value == (2 * meas1).value + assert not_identical_sum != 2 * meas1 + assert np.isclose(not_identical_sum.dvalue, np.sqrt(meas1.dvalue ** 2 + meas1_new.dvalue ** 2)) + + assert meas2 * meas2 == meas2 ** 2 + + +def test_square_cov_obs(): + cov = pe.cov_Obs(1, 0.1 ** 2, "testing") + cov2 = cov ** 2 From 779dedf5b3df31b472c9582fe073e61ea7edd378 Mon Sep 17 00:00:00 2001 From: Fabian Joswig Date: Sun, 17 Nov 2024 12:24:38 +0100 Subject: [PATCH 2/3] [Tests] Added test to compare cov_Obs and Meas implementation. --- tests/obs_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/obs_test.py b/tests/obs_test.py index aa9e5bb2..c21a250d 100644 --- a/tests/obs_test.py +++ b/tests/obs_test.py @@ -1484,3 +1484,11 @@ def test_meas(): def test_square_cov_obs(): cov = pe.cov_Obs(1, 0.1 ** 2, "testing") cov2 = cov ** 2 + + +def test_covobs_equal_meas(): + value = 1.1 + standard_error = 0.23 + meas = pe.Meas(value, standard_error) + covo = pe.cov_Obs(value, standard_error ** 2, meas.names[0]) + assert covo == meas From 5c4f44fa010ffdac1037906e7590ec087f3107ee Mon Sep 17 00:00:00 2001 From: Fabian Joswig Date: Wed, 20 Nov 2024 19:19:36 +0100 Subject: [PATCH 3/3] [Feat] Prevent objects with uuid names to be serialized to json. --- pyerrors/input/json.py | 7 +++++++ tests/json_io_test.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/pyerrors/input/json.py b/pyerrors/input/json.py index ca3fb0d2..1273db0f 100644 --- a/pyerrors/input/json.py +++ b/pyerrors/input/json.py @@ -59,6 +59,8 @@ def _gen_data_d_from_list(ol): def _gen_cdata_d_from_list(ol): dl = [] for name in ol[0].cov_names: + if _is_uuid4_hex(name): + raise ValueError("Cannot safely serialize an Obs derived from a Meas object with a uuid as name. Consider recreating the Meas with an explict name.") ed = {} ed['id'] = name ed['layout'] = str(ol[0].covobs[name].cov.shape).lstrip('(').rstrip(')').rstrip(',') @@ -216,6 +218,11 @@ def _jsonifier(obj): return json.dumps(d, indent=indent, ensure_ascii=False, default=_jsonifier, write_mode=json.WM_COMPACT) +def _is_uuid4_hex(s): + uuid4_hex_pattern = re.compile(r'^[0-9a-f]{32}$') + return bool(uuid4_hex_pattern.match(s)) + + def dump_to_json(ol, fname, description='', indent=1, gz=True): """Export a list of Obs or structures containing Obs to a .json(.gz) file. Dict keys that are not JSON-serializable such as floats are converted to strings. diff --git a/tests/json_io_test.py b/tests/json_io_test.py index dafaaa41..428704fc 100644 --- a/tests/json_io_test.py +++ b/tests/json_io_test.py @@ -409,3 +409,14 @@ def assert_equal_Obs(to, ro): print(kw, "does not match.") return False return True + + +def test_meas_uuid(tmp_path): + meas = pe.Meas(0.3, 0.2) + + for obs in [meas, meas + 1, meas * pe.pseudo_Obs(0.1, 0.001, "obs|r1")]: + with pytest.raises(ValueError): + jsonio.dump_to_json([obs], "test_file", indent=1, description='[This file should not be writable]') + + name_meas = pe.Meas(0.3, 0.2, name="my name") + jsonio.dump_to_json([name_meas], "test_file", indent=1, description='[This file should be writable]')