Skip to content

plot

color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color'] module-attribute

VOLUME_FRACTION_STYLES = ('area', 'line') module-attribute

VolumeFractionProfile dataclass

Volume fraction of every component of a sample versus depth.

Attributes:

Name Type Description
z np.ndarray

Depth in Å; z = 0 at the superphase / first layer interface.

fractions dict

Component key -> phi_c(z). Keys are stable identifiers (the unique_name of the material, or of the first member of a merged group), in order of first appearance from the front of the sample.

labels dict

Component key -> display label, unique within the profile.

Source code in src/easyreflectometry/sample/volume_fraction.py
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
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
202
203
@dataclass
class VolumeFractionProfile:
    """Volume fraction of every component of a sample versus depth.

    Attributes
    ----------
    z : np.ndarray
        Depth in Å; ``z = 0`` at the superphase / first layer interface.
    fractions : dict
        Component key -> ``phi_c(z)``. Keys are stable identifiers (the
        ``unique_name`` of the material, or of the first member of a merged
        group), in order of first appearance from the front of the sample.
    labels : dict
        Component key -> display label, unique within the profile.
    """

    z: np.ndarray
    fractions: dict
    labels: dict

    @property
    def components(self) -> list:
        """The component keys, front to back."""
        return list(self.fractions)

    @property
    def total(self) -> np.ndarray:
        """``sum_c phi_c(z)``; ones up to floating point error."""
        if not self.fractions:
            return np.zeros_like(self.z)
        return np.sum(np.array(list(self.fractions.values())), axis=0)

    def grouped(self, groups: Mapping[str, Sequence[str]]) -> 'VolumeFractionProfile':
        """Merge components into named groups for presentation.

        Parameters
        ----------
        groups : Mapping[str, Sequence[str]]
            Group label -> members, given as component keys or labels, e.g.
            ``{'Heads': ['C10H18NO8P'], 'Water': ['D2O', 'H2O']}``. Each
            component may appear at most once over all groups. Components not
            listed keep their own trace. The result lists the groups first, in
            the given order, then the remaining components in their original
            order.

        Returns
        -------
        VolumeFractionProfile
            A new profile; this one is left unchanged.

        Raises
        ------
        ValueError
            A member is unknown, a component is listed twice, a group is
            empty, or a group label collides with a retained component.
        """
        label_to_key = {label: key for key, label in self.labels.items()}
        used: dict = {}
        fractions: dict = {}
        labels: dict = {}
        for group_label, members in groups.items():
            keys = []
            for member in members:
                key = member if member in self.fractions else label_to_key.get(member)
                if key is None:
                    raise ValueError(
                        f'Unknown component {member!r} in group {group_label!r}. '
                        f'Available keys: {list(self.fractions)}; labels: {list(self.labels.values())}.'
                    )
                if key in used:
                    raise ValueError(
                        f'Component {self.labels[key]!r} is listed in group {group_label!r} '
                        f'and already in group {used[key]!r}; groups must not overlap.'
                    )
                used[key] = group_label
                keys.append(key)
            if not keys:
                raise ValueError(f'Group {group_label!r} has no members.')
            fractions[group_label] = np.sum(np.array([self.fractions[key] for key in keys]), axis=0)
            labels[group_label] = group_label
        for key, values in self.fractions.items():
            if key in used:
                continue
            label = self.labels[key]
            if key in fractions or label in labels.values():
                raise ValueError(
                    f'Group label {label!r} collides with the retained component {label!r}; '
                    'include that component in the group or rename the group.'
                )
            fractions[key] = values.copy()
            labels[key] = label
        return VolumeFractionProfile(z=self.z.copy(), fractions=fractions, labels=labels)

components property

The component keys, front to back.

total property

sum_c phi_c(z); ones up to floating point error.

grouped(groups)

Merge components into named groups for presentation.

Parameters:

Name Type Description Default
groups Mapping[str, Sequence[str]]

Group label -> members, given as component keys or labels, e.g. {'Heads': ['C10H18NO8P'], 'Water': ['D2O', 'H2O']}. Each component may appear at most once over all groups. Components not listed keep their own trace. The result lists the groups first, in the given order, then the remaining components in their original order.

required

Returns:

Type Description
VolumeFractionProfile

A new profile; this one is left unchanged.

Raises:

Type Description
ValueError

A member is unknown, a component is listed twice, a group is empty, or a group label collides with a retained component.

Source code in src/easyreflectometry/sample/volume_fraction.py
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
202
203
def grouped(self, groups: Mapping[str, Sequence[str]]) -> 'VolumeFractionProfile':
    """Merge components into named groups for presentation.

    Parameters
    ----------
    groups : Mapping[str, Sequence[str]]
        Group label -> members, given as component keys or labels, e.g.
        ``{'Heads': ['C10H18NO8P'], 'Water': ['D2O', 'H2O']}``. Each
        component may appear at most once over all groups. Components not
        listed keep their own trace. The result lists the groups first, in
        the given order, then the remaining components in their original
        order.

    Returns
    -------
    VolumeFractionProfile
        A new profile; this one is left unchanged.

    Raises
    ------
    ValueError
        A member is unknown, a component is listed twice, a group is
        empty, or a group label collides with a retained component.
    """
    label_to_key = {label: key for key, label in self.labels.items()}
    used: dict = {}
    fractions: dict = {}
    labels: dict = {}
    for group_label, members in groups.items():
        keys = []
        for member in members:
            key = member if member in self.fractions else label_to_key.get(member)
            if key is None:
                raise ValueError(
                    f'Unknown component {member!r} in group {group_label!r}. '
                    f'Available keys: {list(self.fractions)}; labels: {list(self.labels.values())}.'
                )
            if key in used:
                raise ValueError(
                    f'Component {self.labels[key]!r} is listed in group {group_label!r} '
                    f'and already in group {used[key]!r}; groups must not overlap.'
                )
            used[key] = group_label
            keys.append(key)
        if not keys:
            raise ValueError(f'Group {group_label!r} has no members.')
        fractions[group_label] = np.sum(np.array([self.fractions[key] for key in keys]), axis=0)
        labels[group_label] = group_label
    for key, values in self.fractions.items():
        if key in used:
            continue
        label = self.labels[key]
        if key in fractions or label in labels.values():
            raise ValueError(
                f'Group label {label!r} collides with the retained component {label!r}; '
                'include that component in the group or rename the group.'
            )
        fractions[key] = values.copy()
        labels[key] = label
    return VolumeFractionProfile(z=self.z.copy(), fractions=fractions, labels=labels)

plot_volume_fraction(profile, ax=None, *, style='area', colors=None)

Draw a volume fraction (occupancy) profile, one trace per component.

Parameters:

Name Type Description Default
profile VolumeFractionProfile

The profile, from :meth:easyreflectometry.sample.Sample.volume_fraction_profile or :func:easyreflectometry.sample.volume_fraction_profile.

required
ax plt.Axes

Axes to draw on. A new figure is created when omitted.

None
style str

'area' (default) draws each component as a line with a translucent fill down to zero, the traces overlapping rather than stacked; 'line' draws lines only.

'area'
colors Mapping[str, str]

Colour per component key or label; the matplotlib colour cycle is used for the rest.

None

Returns:

Type Description
plt.Axes

The axes drawn on. The lower y-limit drops below zero when a fraction does, so an inadmissible profile is visible rather than clipped.

Raises:

Type Description
ValueError

style is not one of :data:VOLUME_FRACTION_STYLES.

Source code in src/easyreflectometry/plot.py
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
def plot_volume_fraction(
    profile: VolumeFractionProfile,
    ax: Optional[plt.Axes] = None,
    *,
    style: str = 'area',
    colors: Optional[Mapping[str, str]] = None,
) -> plt.Axes:
    """Draw a volume fraction (occupancy) profile, one trace per component.

    Parameters
    ----------
    profile : VolumeFractionProfile
        The profile, from :meth:`easyreflectometry.sample.Sample.volume_fraction_profile`
        or :func:`easyreflectometry.sample.volume_fraction_profile`.
    ax : plt.Axes, optional
        Axes to draw on. A new figure is created when omitted.
    style : str, optional
        ``'area'`` (default) draws each component as a line with a translucent
        fill down to zero, the traces overlapping rather than stacked; ``'line'``
        draws lines only.
    colors : Mapping[str, str], optional
        Colour per component key or label; the matplotlib colour cycle is used
        for the rest.

    Returns
    -------
    plt.Axes
        The axes drawn on. The lower y-limit drops below zero when a fraction
        does, so an inadmissible profile is visible rather than clipped.

    Raises
    ------
    ValueError
        ``style`` is not one of :data:`VOLUME_FRACTION_STYLES`.
    """
    if style not in VOLUME_FRACTION_STYLES:
        raise ValueError(f'Unknown style {style!r}; choose one of {VOLUME_FRACTION_STYLES}.')
    if ax is None:
        _, ax = plt.subplots(figsize=(6, 3.5))
    colors = colors or {}
    for i, (key, values) in enumerate(profile.fractions.items()):
        label = profile.labels.get(key, key)
        color = colors.get(key, colors.get(label, color_cycle[i % len(color_cycle)]))
        ax.plot(profile.z, values, linestyle='-', color=color, label=label)
        if style == 'area':
            ax.fill_between(profile.z, 0.0, values, color=color, alpha=0.3, linewidth=0)
    lowest = min((float(np.min(values)) for values in profile.fractions.values()), default=0.0)
    ax.set_ylim(min(0.0, lowest) - 0.02, 1.05)
    ax.set_xlabel('z (Å)')
    ax.set_ylabel('Volume fraction')
    if profile.fractions:
        ax.legend()
    return ax

plot(data)

A general plotting function for easyreflectometry.

Parameters:

Name Type Description Default
data sc.DataGroup

The DataGroup to be plotted.

required
Source code in src/easyreflectometry/plot.py
 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
108
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
def plot(data: sc.DataGroup) -> None:
    """A general plotting function for easyreflectometry.

    Parameters
    ----------
    data : sc.DataGroup
        The DataGroup to be plotted.
    """
    if len([i for i in list(data.keys()) if 'SLD' in i]) == 0:
        plot_sld = False
        fig = plt.figure(figsize=(5, 3))
        gs = GridSpec(1, 1, figure=fig)
    else:
        plot_sld = True
        fig = plt.figure(figsize=(5, 6))
        gs = GridSpec(2, 1, figure=fig)
        ax2 = fig.add_subplot(gs[1, 0])
    ax1 = fig.add_subplot(gs[0, 0])
    refl_nums = [k[3:] for k in data['coords'].keys() if 'Qz' == k[:2]]
    for i, refl_num in enumerate(refl_nums):
        plot_data = sc.DataArray(
            name=f'R_{refl_num}',
            data=data['data'][f'R_{refl_num}'].copy(),
            coords={f'Qz_{refl_num}': data['coords'][f'Qz_{refl_num}'].copy()},
        )
        plot_data.data *= sc.scalar(10.0**i, unit=plot_data.unit)
        plot_data.coords[f'Qz_{refl_num}'].variances = None
        sc.plot(plot_data, ax=ax1, norm='log', linestyle='', marker='.', color=color_cycle[i])
        try:
            plot_model_data = sc.DataArray(
                name=f'R_{refl_num}_model',
                data=data[f'R_{refl_num}_model'].copy(),
                coords={f'Qz_{refl_num}': data['coords'][f'Qz_{refl_num}'].copy()},
            )
            plot_model_data.data *= sc.scalar(10.0**i, unit=plot_model_data.unit)
            plot_model_data.coords[f'Qz_{refl_num}'].variances = None
            sc.plot(
                plot_model_data,
                ax=ax1,
                norm='log',
                linestyle='--',
                color=color_cycle[i],
                marker='',
            )
        except KeyError:
            pass
    ax1.autoscale(True)
    ax1.relim()
    ax1.autoscale_view()

    if plot_sld:
        for i, refl_num in enumerate(refl_nums):
            plot_sld_data = sc.DataArray(
                name=f'SLD_{refl_num}',
                data=data[f'SLD_{refl_num}'].copy(),
                coords={f'z_{refl_num}': data['coords'][f'z_{refl_num}'].copy()},
            )
            sc.plot(plot_sld_data, ax=ax2, linestyle='-', color=color_cycle[i], marker='')
        ax2.autoscale(True)
        ax2.relim()
        ax2.autoscale_view()