Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/user-guide/amor/amor-reduction.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,15 @@
"metadata": {},
"outputs": [],
"source": [
"from ess.reflectometry.tools import orso_datasets_from_measurements\n",
"from ess.reflectometry.tools import from_measurements\n",
"\n",
"datasets = orso_datasets_from_measurements(\n",
"datasets = from_measurements(\n",
" workflow,\n",
" runs.values(),\n",
" target=orso.OrsoIofQDataset,\n",
" # Optionally scale the curves to overlap using `scale_reflectivity_curves_to_overlap`\n",
" scale_to_overlap=True\n",
" # with the same \"critical edge interval\" that was used before\n",
" scale_to_overlap=(sc.scalar(0.01, unit='1/angstrom'), sc.scalar(0.014, unit='1/angstrom'))\n",
")"
]
},
Expand Down
2 changes: 1 addition & 1 deletion src/ess/amor/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
NeXusComponent,
NeXusDetectorName,
ProtonCurrent,
RawChopper,
RawSampleRotation,
RunType,
SampleRotation,
Expand All @@ -29,7 +30,6 @@
ChopperFrequency,
ChopperPhase,
ChopperSeparation,
RawChopper,
)


Expand Down
1 change: 1 addition & 0 deletions src/ess/amor/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def evaluate_reference_at_sample_coords(
ref = ref.transform_coords(
(
"Q",
"theta",
"wavelength_resolution",
"sample_size_resolution",
"angular_resolution",
Expand Down
4 changes: 0 additions & 4 deletions src/ess/amor/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,4 @@ class ChopperSeparation(sciline.Scope[RunType, sc.Variable], sc.Variable):
"""Distance between the two choppers."""


class RawChopper(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup):
"""Chopper data loaded from nexus file."""


GravityToggle = NewType("GravityToggle", bool)
114 changes: 81 additions & 33 deletions src/ess/reflectometry/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
import sciline
import scipp as sc
import scipy.optimize as opt
from orsopy.fileio.orso import OrsoDataset

from ess.reflectometry import orso
from ess.reflectometry.types import ReflectivityOverQ
from ess.reflectometry.types import (
Filename,
ReducibleData,
ReflectivityOverQ,
SampleRun,
)
from ess.reflectometry.workflow import with_filenames

_STD_TO_FWHM = sc.scalar(2.0) * sc.sqrt(sc.scalar(2.0) * sc.log(sc.scalar(2.0)))

Expand Down Expand Up @@ -279,18 +284,18 @@ def combine_curves(
)


def orso_datasets_from_measurements(
def from_measurements(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we think of a better name? I don't have great suggestions, I thought of something like batch_reduction, but it's not super descriptive either...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure either. I would like to avoid reduction in the name because I feel the term is already overloaded in our context, and strictly speaking you don't have to request a "reduced" data set here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked copilot, and the suggestion was compute_reflectivity_datasets.
I guess it's because of the last thing in the docstring that says

    Returns
    ---------
    list of the computed ORSO datasets, containing one reflectivity curve each

which I guess should be changed if you can request other data than reflectivity curves?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's true, I'll change it.
By the way, I asked Nico earlier today and he's thinking about a better name, so we are probably going to have a suggestion from him soon.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed some options and we came up with compute_pipeline_targets or get_workflow_targets. They both seem reasonably descriptive to me but maybe the shorter one is better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be annoying, but those names seem very similar to Sciline's pipeline.compute and pipeline.get methods, so it is not very clear what this function is bringing on top of Sciline's methods.

In addition, if this is a function that is supposed to compute any kind of results/targets, then the scale_to_overlap argument seems misplaced, as I'm assuming the scaling could only be applied to a particular type of results (the reflectivity curves).

It feels like the scope of the helper is too wide.
Can we instead either:

  1. make a function that specifically computes relfectivity curves (what most users will want most of the time)
    or
  2. make a function that maps workflows over the different runs and angles, and we then have to compute the targets we want from that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be annoying, but those names seem very similar to Sciline's pipeline.compute and pipeline.get methods, so it is not very clear what this function is bringing on top of Sciline's methods.

Agreed that the name is very similar to scilines .get and .compute methods. It is a bit confusing. I'm ready to go with the batch_reduction name if that is what you prefer.

In addition, if this is a function that is supposed to compute any kind of results/targets, then the scale_to_overlap argument seems misplaced, as I'm assuming the scaling could only be applied to a particular type of results (the reflectivity curves)

It's true that the scale_to_overlap argument isn't relevant for all results/targets. But to say it's only relevant for reflectivity curves is incorrect. It is relevant for all results that display some sort of intensity from the measurements, normalized or not.

make a function that specifically computes reflectivity curves (what most users will want most of the time)

Do you mean the data array containing the reflectivity curve, or the ORSO dataset containing the reflectivity curve?

Why do you think that is better to limit the set of targets it applies to? Do you think the current user interface is error prone? In that case, can you provide a concrete example?

I think the interface is quite unobtrusive, if the user only wants a reflectivity curve they will just only ever pass the ReflectivityOverQ target to the function, and don't even have to be aware that it can do anything else.

make a function that maps workflows over the different runs and angles, and we then have to compute the targets we want from that?

Isn't this what we considered together and concluded that it was difficult to do with sciline currently? I'd be happy to consider this if you made a quick POC showing how it could be done and what the user interface would look like.

Copy link
Member

@nvaytet nvaytet Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I meant was either have a function that only computes things where scale_to_overlap is relevant, or leave the scale_to_overlap out, and have another function that applies it to the resulting data as a second step?

Another thing I thought of was if it was possible to make the function return some sort of proxy object, which would resemble a dict but would have extra methods like out.scale_to_overlap(*args) which could be used to apply the scaling easily, still in a one-liner?

I guess that's maybe sort of what I meant by

make a function that maps workflows over the different runs and angles

Can we make the function do most of the init_workflow work to prepare the pipelines, and then the object it returns could have additional methods like .scale_to_overlap() or .compute(Target), which would allow the user to compute any target for a whole bunch of grouped or ungrouped measurements?

So as opposed to

Isn't this what we considered together and concluded that it was difficult to do with sciline currently?

it wouldn't be mapping on a pure sciline.Pipeline, but would do what your function does, store an intermediate state in a new custom class which allows to compute anything from there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making it a two step process is problematic because in the first step you don't know if you want to scale the results, and that might mean you compute results that you just throw away because the user was actually interested in the scaled results. If you don't materialize the results in step 1 but instead return some sort of lazy object then the user has to explicitly materialize the results, for no reason, from their perspective, only as a side effect of an optimization in the case when the user wants scaled results.

I agree it's not optimal that the scale_to_overlap argument is not applicable to all kinds of targets that can be requested, but I also find it hard to see that it would cause a problem in practice when using the workflow.
Complicating the interface and the implementation only to avoid this is not worth it IMO.

I implemented the current version in collaboration with Nico to address the issues he and I experienced when using the workflow.
This PR has been so delayed that we've now lost several months of potential feedback and improvements, to add a small helper function in the periphery of the application, and now you want a rewrite with a new experimental proxy object interface.

In my opinion it would be best to just merge this or a lightly modified version of it, and then if you think there are opportunities for significant gains in UX by doing something different you can implement that and open a PR.
If you don't agree I think it's better that we sit down and sort this out together in person.

workflow: sciline.Pipeline,
runs: Sequence[Mapping[type, Any]],
runs: Sequence[Mapping[type, Any]] | Mapping[Any, Mapping[type, Any]],
target: type | Sequence[type] = orso.OrsoIofQDataset,
*,
scale_to_overlap: bool = True,
) -> list[OrsoDataset]:
'''Produces a list of ORSO datasets containing one
reflectivity curve for each of the provided runs.
scale_to_overlap: bool | tuple[sc.Variable, sc.Variable] = False,
) -> list | Mapping:
'''Produces a list of datasets containing for each of the provided runs.
Each entry of :code:`runs` is a mapping of parameters and
values needed to produce the dataset.

Optionally, the reflectivity curves can be scaled to overlap in
Optionally, the results can be scaled so that the reflectivity curves overlap in
the regions where they have the same Q-value.

Parameters
Expand All @@ -299,38 +304,81 @@ def orso_datasets_from_measurements(
The sciline workflow used to compute `ReflectivityOverQ` for each of the runs.

runs:
The sciline parameters to be used for each run
The sciline parameters to be used for each run.

target:
The domain type(s) to compute for each run.

scale_to_overlap:
If True the curves will be scaled to overlap.
Note that the curve of the first run is unscaled and
the rest are scaled to match it.
If ``True`` the curves will be scaled to overlap.
If a tuple then this argument will be passed as the ``critical_edge_interval``
argument to the ``scale_reflectivity_curves_to_overlap`` function.

Returns
---------
list of the computed ORSO datasets, containing one reflectivity curve each
'''
reflectivity_curves = []
for parameters in runs:
names = runs.keys() if hasattr(runs, 'keys') else None
runs = runs.values() if hasattr(runs, 'values') else runs

def init_workflow(workflow, parameters):
wf = workflow.copy()
for name, value in parameters.items():
wf[name] = value
reflectivity_curves.append(wf.compute(ReflectivityOverQ))

scale_factors = (
scale_reflectivity_curves_to_overlap([r.hist() for r in reflectivity_curves])[1]
if scale_to_overlap
else (1,) * len(runs)
)
for tp, value in parameters.items():
if tp is Filename[SampleRun]:
continue
wf[tp] = value

if Filename[SampleRun] in parameters:
if isinstance(parameters[Filename[SampleRun]], list | tuple):
wf = with_filenames(
wf,
SampleRun,
parameters[Filename[SampleRun]],
)
else:
wf[Filename[SampleRun]] = parameters[Filename[SampleRun]]
return wf

if scale_to_overlap:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed during the standup, I don't really get why we need the helper function, as opposed to just having different workflows.

  • You would have a workflow that computes a reflectivity curve.
  • If you want to compute this for multiple runs, you just map over the run numbers or filenames, and then use compute_mapped
  • If you want to scale_to_overlap, you have a workflow that basically does the mapping and adds a provider that either just scales the results or scales and merges. You then compute a different result (e.g. CombinedScaledReflectivityOverQ)

You can then view the graph of what is being done for everything (because with the helper function you can't see it all, only pieces, and they are not easy to get to either if you just installed the package?)

In any case, we should avoid having a long discussion here on github, this was just to have a starting point for an in-person discussion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible we can implement the same thing using sciline map reduce. Let us see the helper in this PR as a reference implementation and try to re-implement as one big workflow.

results = []
for parameters in runs:
wf = init_workflow(workflow, parameters)
results.append(wf.compute((ReflectivityOverQ, ReducibleData[SampleRun])))

scale_factors = scale_reflectivity_curves_to_overlap(
[r[ReflectivityOverQ].hist() for r in results],
critical_edge_interval=scale_to_overlap
if isinstance(scale_to_overlap, list | tuple)
else None,
)[1]

datasets = []
for parameters, curve, scale_factor in zip(
runs, reflectivity_curves, scale_factors, strict=True
):
wf = workflow.copy()
for name, value in parameters.items():
wf[name] = value
wf[ReflectivityOverQ] = scale_factor * curve
dataset = wf.compute(orso.OrsoIofQDataset)
for i, parameters in enumerate(runs):
wf = init_workflow(workflow, parameters)
if scale_to_overlap:
# Optimization in case we can avoid re-doing some work:
# Check if any of the targets need ReducibleData if
# ReflectivityOverQ already exists.
# If they don't, we can avoid recomputing ReducibleData.
targets = target if hasattr(target, '__len__') else (target,)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there ever be a danger that someone passed a string as target and the __len__ check fails? I guess if they passed a string, they will have other problems earlier? (target not found in graph)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, but like you said, passing a string is illegal anyway and will lead to error at some point.

if any(
_workflow_needs_quantity_A_even_if_quantity_B_is_set(
wf[t], ReducibleData[SampleRun], ReflectivityOverQ
)
for t in targets
):
wf[ReducibleData[SampleRun]] = (
scale_factors[i] * results[i][ReducibleData[SampleRun]]
)
else:
wf[ReflectivityOverQ] = scale_factors[i] * results[i][ReflectivityOverQ]

dataset = wf.compute(target)
datasets.append(dataset)
return datasets
return datasets if names is None else dict(zip(names, datasets, strict=True))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here is my proposal.

Step 1: add scaling param to workflow

Add an extra parameter in the base workflow called ScalingFactorForOverlap (see implementation in new branch here) which is used to scale the counts of ReducibleData, and set to 1 by default
Screenshot_20250704_105942

Step 2: create PipelineCollection

class PipelineCollection:

    def __init__(self, pipelines):
        self._pipelines = {name: pl.copy() for name, pl in pipelines.items()}

    def __setitem__(self, key, value):
        for name, v in value.items():
            self._pipelines[name][key] = v

    def compute(self, target):
        return {name: pl.compute(target) for name, pl in self._pipelines.items()}

    def copy(self):
        return self.__class__(self._pipelines)

This will be used to compute multiple results in one go, and set params on multiple pipelines in one go.

Step 3: generate the pipeline collection

This code is basically the content of from_measurements, without the handling of scale_to_overlap.

def batch_processor(workflow, runs):
    workflows = {}
    for name, parameters in runs.items():
        wf = workflow.copy()
        for tp, value in parameters.items():
            if tp is Filename[SampleRun]:
                continue
            wf[tp] = value

        if Filename[SampleRun] in parameters:
            if isinstance(parameters[Filename[SampleRun]], list | tuple):
                wf = with_filenames(
                    wf,
                    SampleRun,
                    parameters[Filename[SampleRun]],
                )
            else:
                wf[Filename[SampleRun]] = parameters[Filename[SampleRun]]
        workflows[name] = wf
    return PipelineCollection(workflows)

Use the pipeline collection

wf = pipeline_with_1632_reference()  # original Amor workflow for a single run

# Run params for 3 runs whose event lists should be combined
run = {
    Filename[SampleRun]: list(map(data.amor_run, (1636, 1639, 1641))),
    QBins: sc.geomspace(
        dim='Q', start=0.062, stop=0.18, num=391, unit='1/angstrom'
    ),
    DetectorRotation[SampleRun]: sc.scalar(0.140167, unit='rad'),
    SampleRotation[SampleRun]: sc.scalar(0.0680678, unit='rad'),
}

pc = batch_processor(wf, {'a': run})
pc.compute(ReflectivityOverQ)  # or compute any target you wish.

Step 4: Scale to overlap

Instead of sending reflectivity curves to the function, send the pipeline that can cache results to avoid re-computing them.

# Dummy implementation
def scale_reflectivity_curves_to_overlap(
    pipeline_collection,
    critical_edge_interval: tuple[sc.Variable, sc.Variable] | None = None,
    cache_intermediate_results: bool = True
):

    pc = pipeline_collection.copy()
    if cache_intermediate_results:
        pc[UnscaledReducibleData[SampleRun]] = pc.compute(UnscaledReducibleData[SampleRun])
        
    scaling_factors = {name: np.random.random() for name in pc._pipelines.keys()}

    pc[ScalingFactorForOverlap[SampleRun]] = scaling_factors
    return pc

Now use the scaling function which basically sets the ScalingFactorForOverlap on the pipelines.

new_pc = scale_reflectivity_curves_to_overlap(pc)

new_pc.compute(ReflectivityOverQ)  # Results scaled

Inspect the scaling factors that were used

new_pc.compute(ScalingFactorForOverlap[SampleRun])

Can also write as a one-liner

results = scale_reflectivity_curves_to_overlap(
    batch_processor(wf, {'a': run})
).compute(ReflectivityOverQ)

Pros

  • Removes the complexity in the implementation of from_measurements that handles the scale_to_overlap, including the _workflow_needs_quantity_A_even_if_quantity_B_is_set
  • Removes the need to return both scaled results and scaled factors from scale_reflectivity_curves_to_overlap
  • Improves provenance because we can see in the graph what scaling factor was used
  • Resembles ways of working in other packages because PipelineCollection behaves like a pipeline

Cons

  • Could be confusing for users to have an object that looks like a pipeline but isn't a pipeline?
  • Users have to learn about pipelines, and how they work, to fully understand, as opposed to just calling a python function with args (but there is nothing stopping scientists from writing their own small wrapper around this mechanism?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am all in favor of building higher-level abstractions around sciline.Pipeline. Another example is StreamProcessor, and we need more like that. I think by know most of our team experienced that sciline.Pipeline is a bit too low level for all scientists to understand (and I think this is why @jokasimr is writing helper scripts/functions).

Also, I don't think we should see sciline.Pipeline as single workflow graph that does everything. For example, the map/reduce approach can be a bit clumsy and is somewhat limited, so don't try to use it as the one tool that fits all. Again, this relates to my recent thoughts on needing some higher-level workflow management/engine at ESS/DMSC.

I don't think all this necessarily should (or can) be done in this PR. And I did not look much into the details of the PipelineCollection you propose. I agree with most of your other concerns about the current PR though.

Copy link
Contributor Author

@jokasimr jokasimr Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just a few comments on this:

I think by know most of our team experienced that sciline.Pipeline is a bit too low level for all scientists to understand (and I think this is why @jokasimr is writing helper scripts/functions).

It's not that it's too hard to understand for the scientists but that it is hard to use in these kind of "daily usage" meta workflow scenarios.

Pros (1) Removes the complexity in the implementation of from_measurements that handles the scale_to_overlap, including the _workflow_needs_quantity_A_even_if_quantity_B_is_set

Note that the only reason that part of the code exists is as an optimization to avoid computing the reflectivity curves twice (once to do the scaling to overlap, and then again when actually computing the desired results).
If that part is the problem we can just remove it and pay the 2x performance loss (as is done in the implementation suggested by @nvaytet).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood the mechanism correctly, you only save the double computation when the target requested is the 1D Reflectivity curve or something further down the workflow (like an orso dataset).

If you request something earlier, like a 2d intensity as a function of theta and Q, you still have to compute the 1d curve to get the scaling factor, and then compute the 2d intensity again?

Copy link
Contributor Author

@jokasimr jokasimr Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood the mechanism correctly, you only save the double computation when the target requested is the 1D Reflectivity curve or something further down the workflow (like an orso dataset).

Yes exactly, and computing a orso dataset or a 1d curve is the most common case.



def _workflow_needs_quantity_A_even_if_quantity_B_is_set(workflow, A, B):
wf = workflow.copy()
wf[B] = 'Not important'
return A in wf.underlying_graph
4 changes: 4 additions & 0 deletions src/ess/reflectometry/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
CoordTransformationGraph = NewType("CoordTransformationGraph", dict)


class RawChopper(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup):
"""Chopper data loaded from nexus file."""


class ReducibleData(sciline.Scope[RunType, sc.DataArray], sc.DataArray):
"""Event data with common coordinates added"""

Expand Down
40 changes: 30 additions & 10 deletions src/ess/reflectometry/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
import sciline
import scipp as sc

from ess.amor.types import RawChopper
from ess.reflectometry.orso import (
OrsoExperiment,
OrsoOwner,
OrsoSample,
OrsoSampleFilenames,
)
from ess.reflectometry.types import (
DetectorRotation,
Filename,
RawChopper,
ReducibleData,
RunType,
SampleRotation,
Expand Down Expand Up @@ -62,15 +63,34 @@ def with_filenames(

mapped = wf.map(df)

wf[ReducibleData[runtype]] = mapped[ReducibleData[runtype]].reduce(
index=axis_name, func=_concatenate_event_lists
)
wf[RawChopper[runtype]] = mapped[RawChopper[runtype]].reduce(
index=axis_name, func=_any_value
)
wf[SampleRotation[runtype]] = mapped[SampleRotation[runtype]].reduce(
index=axis_name, func=_any_value
)
try:
wf[ReducibleData[runtype]] = mapped[ReducibleData[runtype]].reduce(
index=axis_name, func=_concatenate_event_lists
)
except ValueError:
# ReducibleData[runtype] is independent of Filename[runtype]
pass
try:
wf[RawChopper[runtype]] = mapped[RawChopper[runtype]].reduce(
index=axis_name, func=_any_value
)
except ValueError:
# RawChopper[runtype] is independent of Filename[runtype]
pass
try:
wf[SampleRotation[runtype]] = mapped[SampleRotation[runtype]].reduce(
index=axis_name, func=_any_value
)
except ValueError:
# SampleRotation[runtype] is independent of Filename[runtype]
pass
try:
wf[DetectorRotation[runtype]] = mapped[DetectorRotation[runtype]].reduce(
index=axis_name, func=_any_value
)
except ValueError:
# DetectorRotation[runtype] is independent of Filename[runtype]
pass

if runtype is SampleRun:
wf[OrsoSample] = mapped[OrsoSample].reduce(index=axis_name, func=_any_value)
Expand Down
68 changes: 68 additions & 0 deletions tests/amor/tools_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
import pytest
import sciline
import scipp as sc
from scipp.testing import assert_allclose

from amor.pipeline_test import amor_pipeline # noqa: F401
from ess.amor import data
from ess.amor.types import ChopperPhase
from ess.reflectometry.tools import from_measurements
from ess.reflectometry.types import (
DetectorRotation,
Filename,
QBins,
ReducedReference,
ReferenceRun,
ReflectivityOverQ,
SampleRotation,
SampleRun,
)

# The files used in the AMOR reduction workflow have some scippnexus warnings
pytestmark = pytest.mark.filterwarnings(
"ignore:.*Invalid transformation, .*missing attribute 'vector':UserWarning",
)


@pytest.fixture
def pipeline_with_1632_reference(amor_pipeline): # noqa: F811
amor_pipeline[ChopperPhase[ReferenceRun]] = sc.scalar(7.5, unit='deg')
amor_pipeline[ChopperPhase[SampleRun]] = sc.scalar(7.5, unit='deg')
amor_pipeline[Filename[ReferenceRun]] = data.amor_run('1632')
amor_pipeline[ReducedReference] = amor_pipeline.compute(ReducedReference)
return amor_pipeline


@pytestmark
def test_from_measurements_tool_concatenates_event_lists(
pipeline_with_1632_reference: sciline.Pipeline,
):
pl = pipeline_with_1632_reference

run = {
Filename[SampleRun]: list(map(data.amor_run, (1636, 1639, 1641))),
QBins: sc.geomspace(
dim='Q', start=0.062, stop=0.18, num=391, unit='1/angstrom'
),
DetectorRotation[SampleRun]: sc.scalar(0.140167, unit='rad'),
SampleRotation[SampleRun]: sc.scalar(0.0680678, unit='rad'),
}
results = from_measurements(
pl,
[run],
target=ReflectivityOverQ,
scale_to_overlap=False,
)

results2 = []
for fname in run[Filename[SampleRun]]:
pl.copy()
pl[Filename[SampleRun]] = fname
pl[QBins] = run[QBins]
pl[DetectorRotation[SampleRun]] = run[DetectorRotation[SampleRun]]
pl[SampleRotation[SampleRun]] = run[SampleRotation[SampleRun]]
results2.append(pl.compute(ReflectivityOverQ).hist().data)

assert_allclose(sum(results2), results[0].hist().data)
Loading
Loading