Skip to content

measurement

DEFAULT_RESOLUTION_FWHM_PERCENTAGE = 5.0 module-attribute

DataSet1D

Bases: SerializerComponent

Source code in src/easyreflectometry/data/data_store.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class DataSet1D(SerializerComponent):
    def __init__(
        self,
        name: str = 'Series',
        x: Optional[Union[np.ndarray, list]] = None,
        y: Optional[Union[np.ndarray, list]] = None,
        ye: Optional[Union[np.ndarray, list]] = None,
        xe: Optional[Union[np.ndarray, list]] = None,
        model: Optional['Model'] = None,  # delay type checking until runtime (quotes)
        x_label: str = 'x',
        y_label: str = 'y',
        auto_background: bool = True,
        resolution_function: Optional[ResolutionFunction] = None,
    ):
        """Init function.

        Parameters
        ----------
        resolution_function : Optional[ResolutionFunction], optional
            The q-resolution this dataset was measured with (typically a
            ``Pointwise`` built from its sQz column). Fitting and model
            curves evaluated against this dataset smear with it instead of
            the model's resolution. ``None`` (the default) means the model's
            resolution applies; it is not derived from ``xe`` here, see
            :func:`easyreflectometry.data.resolution_from_dataset`.
        """
        self._model = model
        self.resolution_function = resolution_function
        #: Whether :meth:`Project.prepare_fit` fits this experiment by default.
        self.include_in_fit = True
        if y is not None and model is not None and auto_background:
            self._model.background = max(np.min(y), 1e-10)

        if x is None:
            x = np.array([])
        if y is None:
            y = np.array([])
        if ye is None:
            ye = np.zeros_like(x)
        if xe is None:
            xe = np.zeros_like(x)

        if len(x) != len(y):
            raise ValueError('x and y must be the same length')

        self.name = name
        if not isinstance(x, np.ndarray):
            x = np.array(x)
        if not isinstance(y, np.ndarray):
            y = np.array(y)
        if not isinstance(ye, np.ndarray):
            ye = np.array(ye)
        if not isinstance(xe, np.ndarray):
            xe = np.array(xe)

        self.x = x
        self.y = y
        self.ye = ye
        self.xe = xe

        self.x_label = x_label
        self.y_label = y_label

        self._color = None

    @property
    def model(self) -> 'Model':  # delay type checking until runtime (quotes)
        """Model function."""
        return self._model

    @model.setter
    def model(self, new_model: 'Model') -> None:
        """Model function."""
        self._model = new_model

    @property
    def is_experiment(self) -> bool:
        """Is experiment."""
        return self._model is not None

    @property
    def is_simulation(self) -> bool:
        """Is simulation."""
        return self._model is None

    def data_points(self) -> tuple[float, float, float, float]:
        """Data points."""
        return zip(self.x, self.y, self.ye, self.xe)

    def __repr__(self) -> str:
        """Repr function."""
        return "1D DataStore of '{:s}' Vs '{:s}' with {} data points".format(self.x_label, self.y_label, len(self.x))

model property writable

Model function.

is_experiment property

Is experiment.

is_simulation property

Is simulation.

__init__(name='Series', x=None, y=None, ye=None, xe=None, model=None, x_label='x', y_label='y', auto_background=True, resolution_function=None)

Init function.

Parameters:

Name Type Description Default
resolution_function Optional[ResolutionFunction]

The q-resolution this dataset was measured with (typically a Pointwise built from its sQz column). Fitting and model curves evaluated against this dataset smear with it instead of the model's resolution. None (the default) means the model's resolution applies; it is not derived from xe here, see :func:easyreflectometry.data.resolution_from_dataset.

None
Source code in src/easyreflectometry/data/data_store.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def __init__(
    self,
    name: str = 'Series',
    x: Optional[Union[np.ndarray, list]] = None,
    y: Optional[Union[np.ndarray, list]] = None,
    ye: Optional[Union[np.ndarray, list]] = None,
    xe: Optional[Union[np.ndarray, list]] = None,
    model: Optional['Model'] = None,  # delay type checking until runtime (quotes)
    x_label: str = 'x',
    y_label: str = 'y',
    auto_background: bool = True,
    resolution_function: Optional[ResolutionFunction] = None,
):
    """Init function.

    Parameters
    ----------
    resolution_function : Optional[ResolutionFunction], optional
        The q-resolution this dataset was measured with (typically a
        ``Pointwise`` built from its sQz column). Fitting and model
        curves evaluated against this dataset smear with it instead of
        the model's resolution. ``None`` (the default) means the model's
        resolution applies; it is not derived from ``xe`` here, see
        :func:`easyreflectometry.data.resolution_from_dataset`.
    """
    self._model = model
    self.resolution_function = resolution_function
    #: Whether :meth:`Project.prepare_fit` fits this experiment by default.
    self.include_in_fit = True
    if y is not None and model is not None and auto_background:
        self._model.background = max(np.min(y), 1e-10)

    if x is None:
        x = np.array([])
    if y is None:
        y = np.array([])
    if ye is None:
        ye = np.zeros_like(x)
    if xe is None:
        xe = np.zeros_like(x)

    if len(x) != len(y):
        raise ValueError('x and y must be the same length')

    self.name = name
    if not isinstance(x, np.ndarray):
        x = np.array(x)
    if not isinstance(y, np.ndarray):
        y = np.array(y)
    if not isinstance(ye, np.ndarray):
        ye = np.array(ye)
    if not isinstance(xe, np.ndarray):
        xe = np.array(xe)

    self.x = x
    self.y = y
    self.ye = ye
    self.xe = xe

    self.x_label = x_label
    self.y_label = y_label

    self._color = None

data_points()

Data points.

Source code in src/easyreflectometry/data/data_store.py
101
102
103
def data_points(self) -> tuple[float, float, float, float]:
    """Data points."""
    return zip(self.x, self.y, self.ye, self.xe)

__repr__()

Repr function.

Source code in src/easyreflectometry/data/data_store.py
105
106
107
def __repr__(self) -> str:
    """Repr function."""
    return "1D DataStore of '{:s}' Vs '{:s}' with {} data points".format(self.x_label, self.y_label, len(self.x))

PercentageFwhm

Bases: ResolutionFunction

Source code in src/easyreflectometry/model/resolution_functions.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class PercentageFwhm(ResolutionFunction):
    def __init__(self, constant: Union[None, float] = None):
        """Init function."""
        if constant is None:
            constant = DEFAULT_RESOLUTION_FWHM_PERCENTAGE
        self.constant = constant

    def smearing(self, q: Union[np.array, float]) -> np.array:
        """Return per-point sigma values from the constant FWHM percentage.

        ``constant`` is a FWHM percentage of ``q``; it is converted to an
        absolute sigma so the smearing() contract is sigma for all types.
        """
        q_array = np.asarray(q, dtype=float)
        fwhm = (self.constant / 100.0) * q_array
        return fwhm / SIGMA_TO_FWHM

    def as_dict(
        self, skip: Optional[List[str]] = None
    ) -> dict[str, str]:  # skip is kept for consistency of the as_dict signature
        """As dict."""
        return {'smearing': 'PercentageFwhm', 'constant': self.constant}

__init__(constant=None)

Init function.

Source code in src/easyreflectometry/model/resolution_functions.py
60
61
62
63
64
def __init__(self, constant: Union[None, float] = None):
    """Init function."""
    if constant is None:
        constant = DEFAULT_RESOLUTION_FWHM_PERCENTAGE
    self.constant = constant

smearing(q)

Return per-point sigma values from the constant FWHM percentage.

constant is a FWHM percentage of q; it is converted to an absolute sigma so the smearing() contract is sigma for all types.

Source code in src/easyreflectometry/model/resolution_functions.py
66
67
68
69
70
71
72
73
74
def smearing(self, q: Union[np.array, float]) -> np.array:
    """Return per-point sigma values from the constant FWHM percentage.

    ``constant`` is a FWHM percentage of ``q``; it is converted to an
    absolute sigma so the smearing() contract is sigma for all types.
    """
    q_array = np.asarray(q, dtype=float)
    fwhm = (self.constant / 100.0) * q_array
    return fwhm / SIGMA_TO_FWHM

as_dict(skip=None)

As dict.

Source code in src/easyreflectometry/model/resolution_functions.py
76
77
78
79
80
def as_dict(
    self, skip: Optional[List[str]] = None
) -> dict[str, str]:  # skip is kept for consistency of the as_dict signature
    """As dict."""
    return {'smearing': 'PercentageFwhm', 'constant': self.constant}

Pointwise

Bases: ResolutionFunction

Pointwise resolution defined by a per-point resolution provided with the data.

The resolution is supplied as the variance of the Qz values (sQz) at the measured Qz data points, which is the form produced by data reduction (e.g. Qz_0.variances). The resolution width at each point is sqrt(sQz). For a requested q the width is obtained by linearly interpolating onto q, exactly as :class:LinearSpline does for explicitly provided widths.

This is a convenience wrapper around :class:LinearSpline that derives the widths from the [Qz, R, sQz] triple loaded from a data file; the returned widths are consumed by the calculators (refnx x_err / refl1d dq), which perform the actual convolution against the model.

Serialization contract: as_dict/from_dict store sQz_data_points as variances (sigma squared). This is deliberately unchanged by the ORSO value_is: FWHM support — FWHM columns are converted to sigma at load time, so stored values are always sigma squared and saved projects round-trip without migration.

Source code in src/easyreflectometry/model/resolution_functions.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
class Pointwise(ResolutionFunction):
    """Pointwise resolution defined by a per-point resolution provided with the data.

    The resolution is supplied as the variance of the Qz values (``sQz``) at the
    measured Qz data points, which is the form produced by data reduction (e.g.
    ``Qz_0.variances``).  The resolution width at each point is ``sqrt(sQz)``.
    For a requested ``q`` the width is obtained by linearly interpolating onto
    ``q``, exactly as :class:`LinearSpline` does for explicitly provided widths.

    This is a convenience wrapper around :class:`LinearSpline` that derives the
    widths from the ``[Qz, R, sQz]`` triple loaded from a data file; the returned
    widths are consumed by the calculators (refnx ``x_err`` / refl1d ``dq``),
    which perform the actual convolution against the model.

    Serialization contract: ``as_dict``/``from_dict`` store ``sQz_data_points``
    as **variances** (sigma squared). This is deliberately unchanged by the
    ORSO ``value_is: FWHM`` support — FWHM columns are converted to sigma at
    load time, so stored values are always sigma squared and saved projects
    round-trip without migration.
    """

    def __init__(self, q_data_points: List[np.ndarray]):
        """Init function.

        Parameters
        ----------
        q_data_points : List[np.ndarray]
            ``[Qz, R, sQz]`` where ``Qz`` are the measured Qz values, ``R`` the
            measured reflectivity (kept only for serialization round-trips) and
            ``sQz`` the variance of ``Qz`` at each point.
        """
        self.q_data_points = q_data_points

    def smearing(self, q: Optional[Union[np.ndarray, float]] = None) -> np.ndarray:
        """Return the resolution sigma interpolated onto ``q``.

        ``sQz`` is the variance of ``Qz``, so the sigma at each data point is
        ``sqrt(sQz)``; values are linearly interpolated onto the requested
        ``q``.  This already satisfies the sigma smearing() contract, so no
        FWHM conversion is applied.  When ``q`` is ``None``, or ``q`` is
        exactly the stored data points, the per-point sigma values are
        returned as they are: a dataset merged from several measurements can
        hold the same ``Qz`` twice with different widths (overlapping angles),
        which no function of ``q`` can represent but a per-point lookup can.
        """
        Qz = np.asarray(self.q_data_points[0], dtype=float)
        sQz = np.asarray(self.q_data_points[2], dtype=float)
        widths = np.sqrt(sQz)
        if q is None:
            return widths.copy()
        q_eval = np.asarray(q, dtype=float)
        if q_eval.shape == Qz.shape and np.array_equal(q_eval, Qz):
            return widths.copy()
        # np.interp needs increasing abscissae; merged datasets are sorted, but
        # a Pointwise built directly from arrays need not be.
        order = np.argsort(Qz, kind='stable')
        return np.asarray(np.interp(q_eval, Qz[order], widths[order]))

    def as_dict(
        self, skip: Optional[List[str]] = None
    ) -> dict[str, str]:  # skip is kept for consistency of the as_dict signature
        """As dict."""
        return {
            'smearing': 'Pointwise',
            'q_data_points': list(self.q_data_points[0]),
            'R_data_points': list(self.q_data_points[1]),
            'sQz_data_points': list(self.q_data_points[2]),
        }

__init__(q_data_points)

Init function.

Parameters:

Name Type Description Default
q_data_points List[np.ndarray]

[Qz, R, sQz] where Qz are the measured Qz values, R the measured reflectivity (kept only for serialization round-trips) and sQz the variance of Qz at each point.

required
Source code in src/easyreflectometry/model/resolution_functions.py
130
131
132
133
134
135
136
137
138
139
140
def __init__(self, q_data_points: List[np.ndarray]):
    """Init function.

    Parameters
    ----------
    q_data_points : List[np.ndarray]
        ``[Qz, R, sQz]`` where ``Qz`` are the measured Qz values, ``R`` the
        measured reflectivity (kept only for serialization round-trips) and
        ``sQz`` the variance of ``Qz`` at each point.
    """
    self.q_data_points = q_data_points

smearing(q=None)

Return the resolution sigma interpolated onto q.

sQz is the variance of Qz, so the sigma at each data point is sqrt(sQz); values are linearly interpolated onto the requested q. This already satisfies the sigma smearing() contract, so no FWHM conversion is applied. When q is None, or q is exactly the stored data points, the per-point sigma values are returned as they are: a dataset merged from several measurements can hold the same Qz twice with different widths (overlapping angles), which no function of q can represent but a per-point lookup can.

Source code in src/easyreflectometry/model/resolution_functions.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def smearing(self, q: Optional[Union[np.ndarray, float]] = None) -> np.ndarray:
    """Return the resolution sigma interpolated onto ``q``.

    ``sQz`` is the variance of ``Qz``, so the sigma at each data point is
    ``sqrt(sQz)``; values are linearly interpolated onto the requested
    ``q``.  This already satisfies the sigma smearing() contract, so no
    FWHM conversion is applied.  When ``q`` is ``None``, or ``q`` is
    exactly the stored data points, the per-point sigma values are
    returned as they are: a dataset merged from several measurements can
    hold the same ``Qz`` twice with different widths (overlapping angles),
    which no function of ``q`` can represent but a per-point lookup can.
    """
    Qz = np.asarray(self.q_data_points[0], dtype=float)
    sQz = np.asarray(self.q_data_points[2], dtype=float)
    widths = np.sqrt(sQz)
    if q is None:
        return widths.copy()
    q_eval = np.asarray(q, dtype=float)
    if q_eval.shape == Qz.shape and np.array_equal(q_eval, Qz):
        return widths.copy()
    # np.interp needs increasing abscissae; merged datasets are sorted, but
    # a Pointwise built directly from arrays need not be.
    order = np.argsort(Qz, kind='stable')
    return np.asarray(np.interp(q_eval, Qz[order], widths[order]))

as_dict(skip=None)

As dict.

Source code in src/easyreflectometry/model/resolution_functions.py
167
168
169
170
171
172
173
174
175
176
def as_dict(
    self, skip: Optional[List[str]] = None
) -> dict[str, str]:  # skip is kept for consistency of the as_dict signature
    """As dict."""
    return {
        'smearing': 'Pointwise',
        'q_data_points': list(self.q_data_points[0]),
        'R_data_points': list(self.q_data_points[1]),
        'sQz_data_points': list(self.q_data_points[2]),
    }

ResolutionFunction

Source code in src/easyreflectometry/model/resolution_functions.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class ResolutionFunction:
    @abstractmethod
    def smearing(self, q: Union[np.array, float]) -> np.array:
        """Return the resolution as sigma (standard deviation) at each ``q``."""
        ...

    @abstractmethod
    def as_dict(self, skip: Optional[List[str]] = None) -> dict: ...

    @classmethod
    def from_dict(cls, data: dict) -> ResolutionFunction:
        """Smearing function."""
        if data['smearing'] == 'PercentageFwhm':
            return PercentageFwhm(data['constant'])
        if data['smearing'] == 'LinearSpline':
            return LinearSpline(data['q_data_points'], data['fwhm_values'])
        if data['smearing'] == 'Pointwise':
            return Pointwise([
                data['q_data_points'],
                data['R_data_points'],
                data['sQz_data_points'],
            ])
        raise ValueError('Unknown resolution function type')

smearing(q) abstractmethod

Return the resolution as sigma (standard deviation) at each q.

Source code in src/easyreflectometry/model/resolution_functions.py
35
36
37
38
@abstractmethod
def smearing(self, q: Union[np.array, float]) -> np.array:
    """Return the resolution as sigma (standard deviation) at each ``q``."""
    ...

from_dict(data) classmethod

Smearing function.

Source code in src/easyreflectometry/model/resolution_functions.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@classmethod
def from_dict(cls, data: dict) -> ResolutionFunction:
    """Smearing function."""
    if data['smearing'] == 'PercentageFwhm':
        return PercentageFwhm(data['constant'])
    if data['smearing'] == 'LinearSpline':
        return LinearSpline(data['q_data_points'], data['fwhm_values'])
    if data['smearing'] == 'Pointwise':
        return Pointwise([
            data['q_data_points'],
            data['R_data_points'],
            data['sQz_data_points'],
        ])
    raise ValueError('Unknown resolution function type')

is_orso_file(fname)

Whether fname is an ORSO file: text with the ORSO banner line, or HDF5 (.orb).

Parameters:

Name Type Description Default
fname str

Path to the file.

required

Returns:

Type Description
bool

True when the file starts with the ORSO banner or the HDF5 magic bytes.

Source code in src/easyreflectometry/orso_utils.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def is_orso_file(fname: str) -> bool:
    """Whether *fname* is an ORSO file: text with the ORSO banner line, or HDF5 (.orb).

    Parameters
    ----------
    fname : str
        Path to the file.

    Returns
    -------
    bool
        True when the file starts with the ORSO banner or the HDF5 magic bytes.
    """
    try:
        with open(fname, 'rb') as f:
            head = f.read(128)
    except OSError:
        return False
    if head.startswith(_HDF5_MAGIC):
        return True
    try:
        first_line = head.decode('utf-8', errors='replace').splitlines()[0]
    except IndexError:
        return False
    return first_line.lstrip().startswith('#') and ORSO_BANNER in first_line

load_data_from_orso_file(fname)

Load data from an ORSO file (.ort text or .orb binary).

Parameters:

Name Type Description Default
fname str

Path to the file.

required

Returns:

Type Description
sc.DataGroup

A scipp DataGroup with data, coords, and attrs.

Raises:

Type Description
ValueError :

If the file cannot be parsed as ORSO. Parse failures are not swallowed here; falling back to plain-text loading is the caller's decision and only valid when the file carries no ORSO banner.

Source code in src/easyreflectometry/orso_utils.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def load_data_from_orso_file(fname: str) -> sc.DataGroup:
    """Load data from an ORSO file (`.ort` text or `.orb` binary).

    Parameters
    ----------
    fname : str
        Path to the file.

    Returns
    -------
    sc.DataGroup
        A scipp DataGroup with data, coords, and attrs.

    Raises
    ------
    ValueError :
        If the file cannot be parsed as ORSO. Parse failures are **not**
        swallowed here; falling back to plain-text loading is the caller's
        decision and only valid when the file carries no ORSO banner.
    """
    orso_data = _load_orso_any(fname)
    return load_orso_data(orso_data)

load(fname)

Load data from an ORSO file (.ort/.orb) or a plain text file.

The discriminator is the ORSO banner line (or the HDF5 magic for binary files), not the file extension: a file carrying the banner that fails to parse raises instead of being silently re-read as plain text (which would drop the entire header, including polarization).

For a plain text file the columns are read by position in the ORSO order Qz, R, sR, sQz -- further numeric columns are ignored -- and the error columns must hold standard deviations, not variances. See :func:_load_txt.

Parameters:

Name Type Description Default
fname Union[TextIO, str]

The file to be read.

required

Returns:

Type Description
sc.DataGroup

The loaded data.

Source code in src/easyreflectometry/data/measurement.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def load(fname: Union[TextIO, str]) -> sc.DataGroup:
    """Load data from an ORSO file (.ort/.orb) or a plain text file.

    The discriminator is the ORSO banner line (or the HDF5 magic for binary
    files), **not** the file extension: a file carrying the banner that fails
    to parse raises instead of being silently re-read as plain text (which
    would drop the entire header, including polarization).

    For a plain text file the columns are read by position in the ORSO order
    Qz, R, sR, sQz -- further numeric columns are ignored -- and the error
    columns must hold **standard deviations**, not variances. See
    :func:`_load_txt`.

    Parameters
    ----------
    fname : Union[TextIO, str]
        The file to be read.

    Returns
    -------
    sc.DataGroup
        The loaded data.
    """
    if is_orso_file(str(fname)):
        return load_data_from_orso_file(fname)
    return _load_txt(fname)

dataset_from_datagroup(data_group, data_key=None)

Build a DataSet1D from one dataset of a loaded DataGroup.

The ORSO header (when present) is attached to the returned dataset as the orso_header attribute (a plain dict), so exporters can reuse the original data_source/reduction provenance.

The measured q-resolution, when the data carry one (positive sQz), is set as the dataset's resolution_function (see :func:resolution_from_dataset), so fitting smears the dataset with the widths it was measured with. Otherwise it stays None and the model's resolution applies.

Parameters:

Name Type Description Default
data_group sc.DataGroup

A DataGroup as returned by :func:load.

required
data_key Optional[str]

The data entry to use (e.g. 'R_0'). By default, the first entry.

None

Returns:

Type Description
DataSet1D

The dataset.

Source code in src/easyreflectometry/data/measurement.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def dataset_from_datagroup(data_group: sc.DataGroup, data_key: Optional[str] = None) -> DataSet1D:
    """Build a DataSet1D from one dataset of a loaded DataGroup.

    The ORSO header (when present) is attached to the returned dataset as the
    ``orso_header`` attribute (a plain dict), so exporters can reuse the
    original ``data_source``/``reduction`` provenance.

    The measured q-resolution, when the data carry one (positive sQz), is set
    as the dataset's ``resolution_function`` (see
    :func:`resolution_from_dataset`), so fitting smears the dataset with the
    widths it was measured with. Otherwise it stays ``None`` and the model's
    resolution applies.

    Parameters
    ----------
    data_group : sc.DataGroup
        A DataGroup as returned by :func:`load`.
    data_key : Optional[str], optional
        The data entry to use (e.g. ``'R_0'``). By default, the first entry.

    Returns
    -------
    DataSet1D
        The dataset.
    """
    if data_key is None:
        data_key = list(data_group['data'].keys())[0]
    coords_key = 'Qz_' + data_key[len('R_') :]
    if coords_key not in data_group['coords']:
        coords_key = list(data_group['coords'].keys())[0]
    dataset = DataSet1D(
        x=data_group['coords'][coords_key].values,
        y=data_group['data'][data_key].values,
        ye=data_group['data'][data_key].variances,
        xe=data_group['coords'][coords_key].variances,
    )
    header = None
    if 'attrs' in data_group and data_key in data_group['attrs']:
        try:
            header = data_group['attrs'][data_key]['orso_header'].values
        except (KeyError, AttributeError):
            header = None
    dataset.orso_header = header
    dataset.resolution_function = resolution_from_dataset(dataset)
    return dataset

load_as_dataset(fname, data_group=None)

Load data from an ORSO .ort file as a DataSet1D.

Parameters:

Name Type Description Default
fname Union[TextIO, str]

The file to be read.

required
data_group Optional[sc.DataGroup]

Pre-loaded DataGroup for fname (avoids re-parsing the file). By default, None.

None

Returns:

Type Description
DataSet1D

The (first) dataset in the file.

Source code in src/easyreflectometry/data/measurement.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def load_as_dataset(fname: Union[TextIO, str], data_group: Optional[sc.DataGroup] = None) -> DataSet1D:
    """Load data from an ORSO .ort file as a DataSet1D.

    Parameters
    ----------
    fname : Union[TextIO, str]
        The file to be read.
    data_group : Optional[sc.DataGroup], optional
        Pre-loaded DataGroup for *fname* (avoids re-parsing the file).
        By default, None.

    Returns
    -------
    DataSet1D
        The (first) dataset in the file.
    """
    if data_group is None:
        data_group = load(fname)
    basename = os.path.splitext(os.path.basename(fname))[0]
    data_name = 'R_' + basename
    data_name = list(data_group['data'].keys())[0] if data_name not in data_group['data'] else data_name
    return dataset_from_datagroup(data_group, data_key=data_name)

extract_orso_title(data_group, data_name)

Extract orso title.

Source code in src/easyreflectometry/data/measurement.py
123
124
125
126
127
128
129
130
131
132
133
def extract_orso_title(data_group: sc.DataGroup, data_name: str) -> str | None:
    """Extract orso title."""
    try:
        header = data_group['attrs'][data_name]['orso_header']
        title = header.values.get('data_source', {}).get('experiment', {}).get('title')
    except (AttributeError, KeyError, TypeError):
        return None
    if title is None:
        return None
    title_str = str(title).strip()
    return title_str or None

_load_txt(fname)

Load data from a simple txt file.

Columns are read by position, following the ORSO order: Qz, R, sR, sQz. Any further numeric columns (e.g. wavelength) are ignored -- the whole file is still parsed as numbers before the leading columns are taken, so a trailing text column, or rows of differing width, remain an error.

The error columns are taken to be standard deviations (sigma), matching the ORSO default, and are squared to obtain the stored variances; a file carrying variances instead would be mis-scaled. Plain text carries no convention marker, so this is a requirement on the caller and not something the loader can check -- unlike an ORSO file, which declares value_is and whose FWHM errors are converted to sigma on load.

Parameters:

Name Type Description Default
fname Union[TextIO, str]

The path for the file to be read.

required
Source code in src/easyreflectometry/data/measurement.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def _load_txt(fname: Union[TextIO, str]) -> sc.DataGroup:
    """Load data from a simple txt file.

    Columns are read by position, following the ORSO order: Qz, R, sR, sQz.
    Any further **numeric** columns (e.g. wavelength) are ignored -- the whole
    file is still parsed as numbers before the leading columns are taken, so a
    trailing text column, or rows of differing width, remain an error.

    The error columns are taken to be **standard deviations** (sigma), matching
    the ORSO default, and are squared to obtain the stored variances; a file
    carrying variances instead would be mis-scaled. Plain text carries no
    convention marker, so this is a requirement on the caller and not something
    the loader can check -- unlike an ORSO file, which declares ``value_is`` and
    whose FWHM errors are converted to sigma on load.

    Parameters
    ----------
    fname : Union[TextIO, str]
        The path for the file to be read.
    """
    # fname can have either a space or a comma as delimiter
    # Determine the delimiter used in the file
    delimiter = None
    with open(fname, 'r') as f:
        # find first non-comment and non-empty line
        for line in f:
            if line.strip() and not line.startswith('#'):
                break
        first_line = line
    if ',' in first_line:
        delimiter = ','

    basename = os.path.splitext(os.path.basename(fname))[0]

    try:
        # ndmin=2 keeps a single-row file two-dimensional, so columns are indexable
        data = np.loadtxt(fname, delimiter=delimiter, comments='#', ndmin=2)
        num_columns = data.shape[1]

        # Verify minimum column requirement
        if num_columns < 3:
            raise ValueError(f'File must contain at least 3 columns (found {num_columns})')

        # Take the leading columns by position; any extra columns are ignored
        if num_columns >= 4:
            x, y, e, xe = data[:, :4].T
        else:  # 3 columns
            x, y, e = data[:, :3].T
            xe = np.zeros_like(x)

    except (ValueError, IOError) as error:
        # Re-raise with more descriptive message
        raise ValueError(f'Failed to load data from {fname}: {str(error)}') from error

    data_name = 'R_' + basename
    coords_name = 'Qz_' + basename
    data = {data_name: sc.array(dims=[coords_name], values=y, variances=np.square(e))}
    coords = {
        data[data_name].dims[0]: sc.array(
            dims=[coords_name],
            values=x,
            variances=np.square(xe),
            unit=sc.Unit('1/angstrom'),
        )
    }
    return sc.DataGroup(data=data, coords=coords)

resolution_from_dataset(dataset)

The measured q-resolution of a dataset, or None when it carries none.

A dataset carries its resolution as the xe column, the variance of Qz (sQz²) at every point. When any of it is positive a :class:Pointwise resolution is built from it; an absent, empty, all-zero or all-NaN column gives None, meaning the model's resolution applies.

Parameters:

Name Type Description Default
dataset DataSet1D

The dataset.

required

Returns:

Type Description
Optional[Pointwise]

The per-point resolution, or None.

Source code in src/easyreflectometry/data/measurement.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def resolution_from_dataset(dataset: DataSet1D) -> Optional[Pointwise]:
    """The measured q-resolution of a dataset, or ``None`` when it carries none.

    A dataset carries its resolution as the ``xe`` column, the variance of
    ``Qz`` (sQz²) at every point. When any of it is positive a
    :class:`Pointwise` resolution is built from it; an absent, empty, all-zero
    or all-NaN column gives ``None``, meaning the model's resolution applies.

    Parameters
    ----------
    dataset : DataSet1D
        The dataset.

    Returns
    -------
    Optional[Pointwise]
        The per-point resolution, or ``None``.
    """
    xe = getattr(dataset, 'xe', None)
    if xe is None or len(xe) == 0:
        return None
    xe = np.asarray(xe, dtype=float)
    # nan-robust gate: nan is truthy for np.any, but a nan-carrying xe must
    # not build a Pointwise (np.interp would propagate the nan everywhere).
    if not np.any(np.nan_to_num(xe) > 0):
        return None
    return Pointwise(q_data_points=[np.asarray(dataset.x, dtype=float), np.asarray(dataset.y, dtype=float), xe])

merge_datasets(datasets, name=None, fill_resolution=None)

Combine several measurements of one contrast into a single dataset.

A contrast is often measured as several curves -- one per incident angle or wavelength band -- each with its own q-resolution. This concatenates them, sorted by Qz, into one dataset that keeps every point's own resolution, so the fit smears each point with the width it was measured with. Overlapping q ranges are kept as they are: the merged dataset may hold the same Qz twice with different widths, which :meth:Pointwise.smearing honours when evaluated at the data points.

Each input contributes the widths of its own resolution_function, evaluated at its points -- the measured :class:Pointwise set by the file loaders, or whatever was assigned explicitly -- so merging does not change the resolution a measurement is fitted with. An input whose resolution_function is None (the model's resolution applies) has no widths of its own; its xe column is not consulted.

Parameters:

Name Type Description Default
datasets Sequence[DataSet1D]

The measurements to combine; at least one. The model, labels and ORSO header of the first one are kept.

required
name Optional[str]

Name of the merged dataset. By default the input names joined by ' + '.

None
fill_resolution Optional[ResolutionFunction]

Resolution assumed for the points of a dataset that carries no resolution when others do, so the merged resolution covers every point. By default the 5% FWHM default resolution. A warning is emitted in that case. When no dataset carries a resolution, the merged dataset has none either and xe is the inputs' columns concatenated.

None

Returns:

Type Description
DataSet1D

The merged dataset. When any input carried a resolution, its resolution_function is a :class:Pointwise over all points and xe holds the matching variances.

Raises:

Type Description
ValueError

No datasets were given.

Source code in src/easyreflectometry/data/measurement.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def merge_datasets(
    datasets: Sequence[DataSet1D],
    name: Optional[str] = None,
    fill_resolution: Optional[ResolutionFunction] = None,
) -> DataSet1D:
    """Combine several measurements of one contrast into a single dataset.

    A contrast is often measured as several curves -- one per incident angle
    or wavelength band -- each with its own q-resolution. This concatenates
    them, sorted by ``Qz``, into one dataset that keeps every point's own
    resolution, so the fit smears each point with the width it was measured
    with. Overlapping q ranges are kept as they are: the merged dataset may
    hold the same ``Qz`` twice with different widths, which
    :meth:`Pointwise.smearing` honours when evaluated at the data points.

    Each input contributes the widths of its own ``resolution_function``,
    evaluated at its points -- the measured :class:`Pointwise` set by the
    file loaders, or whatever was assigned explicitly -- so merging does not
    change the resolution a measurement is fitted with. An input whose
    ``resolution_function`` is ``None`` (the model's resolution applies) has
    no widths of its own; its ``xe`` column is not consulted.

    Parameters
    ----------
    datasets : Sequence[DataSet1D]
        The measurements to combine; at least one. The model, labels and ORSO
        header of the first one are kept.
    name : Optional[str], optional
        Name of the merged dataset. By default the input names joined by ' + '.
    fill_resolution : Optional[ResolutionFunction], optional
        Resolution assumed for the points of a dataset that carries no
        resolution when others do, so the merged resolution covers every
        point. By default the 5% FWHM default resolution. A warning is
        emitted in that case. When no dataset carries a resolution, the
        merged dataset has none either and ``xe`` is the inputs' columns
        concatenated.

    Returns
    -------
    DataSet1D
        The merged dataset. When any input carried a resolution, its
        ``resolution_function`` is a :class:`Pointwise` over all points and
        ``xe`` holds the matching variances.

    Raises
    ------
    ValueError
        No datasets were given.
    """
    datasets = list(datasets)
    if not datasets:
        raise ValueError('At least one dataset is required to merge.')

    resolutions = [getattr(dataset, 'resolution_function', None) for dataset in datasets]
    any_resolution = any(resolution is not None for resolution in resolutions)
    if any_resolution and not all(resolution is not None for resolution in resolutions):
        missing = [dataset.name for dataset, resolution in zip(datasets, resolutions) if resolution is None]
        if fill_resolution is None:
            fill_resolution = PercentageFwhm(DEFAULT_RESOLUTION_FWHM_PERCENTAGE)
        warnings.warn(
            f'Dataset(s) {missing} carry no q-resolution while the others do; their points are '
            f'given the {type(fill_resolution).__name__} resolution so the merged dataset has one.',
            UserWarning,
            stacklevel=2,
        )

    xs, ys, yes, xes = [], [], [], []
    for dataset, resolution in zip(datasets, resolutions):
        x = np.asarray(dataset.x, dtype=float)
        if any_resolution:
            # Evaluated at the dataset's own points, so a Pointwise returns
            # its per-point widths rather than interpolating by q.
            xe = np.square(np.asarray((resolution or fill_resolution).smearing(x), dtype=float))
        else:
            xe = np.zeros_like(x) if dataset.xe is None else np.asarray(dataset.xe, dtype=float)
        xs.append(x)
        ys.append(np.asarray(dataset.y, dtype=float))
        yes.append(np.zeros_like(x) if dataset.ye is None else np.asarray(dataset.ye, dtype=float))
        xes.append(xe)

    x = np.concatenate(xs)
    order = np.argsort(x, kind='stable')
    first = datasets[0]
    merged = DataSet1D(
        name=' + '.join(dataset.name for dataset in datasets) if name is None else name,
        x=x[order],
        y=np.concatenate(ys)[order],
        ye=np.concatenate(yes)[order],
        xe=np.concatenate(xes)[order],
        model=first.model,
        x_label=first.x_label,
        y_label=first.y_label,
        auto_background=False,
    )
    merged.orso_header = getattr(first, 'orso_header', None)
    if any_resolution:
        merged.resolution_function = Pointwise(q_data_points=[merged.x, merged.y, merged.xe])
    return merged

merge_datagroups(*data_groups)

Merge multiple DataGroups into a single DataGroup.

Source code in src/easyreflectometry/data/measurement.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def merge_datagroups(*data_groups: sc.DataGroup) -> sc.DataGroup:
    """Merge multiple DataGroups into a single DataGroup."""
    merged_data = {}
    merged_coords = {}
    merged_attrs = {}

    for group in data_groups:
        for key, value in group['data'].items():
            if key not in merged_data:
                merged_data[key] = value
            else:
                merged_data[key] = sc.concat([merged_data[key], value], dim=merged_data[key].dims[0])

        for key, value in group['coords'].items():
            if key not in merged_coords:
                merged_coords[key] = value
            else:
                merged_coords[key] = sc.concat([merged_coords[key], value], dim=merged_coords[key].dims[0])

        if 'attrs' not in group:
            continue
        for key, value in group['attrs'].items():
            if key not in merged_attrs:
                merged_attrs[key] = value
            else:
                merged_attrs[key] = {**merged_attrs[key], **value}

    return sc.DataGroup(data=merged_data, coords=merged_coords, attrs=merged_attrs)