Skip to content

measurement

DataSet1D

Bases: SerializerComponent

Source code in src/easyreflectometry/data/data_store.py
15
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
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,
    ):
        """Init function."""
        self._model = model
        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)

Init function.

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
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,
):
    """Init function."""
    self._model = model
    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
85
86
87
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
89
90
91
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))

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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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
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
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.

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
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
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.

    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
    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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
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
110
111
112
113
114
115
116
117
118
119
120
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
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
177
178
179
180
181
182
183
184
185
186
187
188
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)

merge_datagroups(*data_groups)

Merge multiple DataGroups into a single DataGroup.

Source code in src/easyreflectometry/data/measurement.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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)