Concentration Correction
========================

Concentration correction compensates for variations in the sample *amount* (e.g.
concentration, beam footprint) that occur between measurements. It works by
dividing each signal and monitor by the corresponding signal and monitor of the
correction scan.

The :meth:`~daxs.measurements.Measurement.concentration_correction` method
chooses between two internal correctors based on the arguments you supply.

Simple concentration correction
--------------------------------

The :class:`~daxs.correctors.SimpleConcentrationCorrector` is activated by
passing **only** the ``indices`` or ``scans`` argument, without an extra
``data_mappings`` dictionary:

.. code-block:: python

    measurement.concentration_correction(indices=101)

Internally, ``daxs`` reads the correction scan(s) from the *same* source as the
measurement scans. The corrector selects the division strategy based on how many
correction scans are provided relative to the measurement scans:

.. list-table::
   :header-rows: 1
   :widths: 35 65

   * - Situation
     - Behaviour
   * - One correction scan with as many data points as there are measurement scans
     - Each data point of the correction scan scales the corresponding measurement scan.
   * - One correction scan, but the numbers above do not match
     - Every measurement scan is divided by the entire correction scan.
   * - As many correction scans as measurement scans
     - Each correction scan is paired one-to-one with a measurement scan.
   * - Any other combination
     - An error is raised.

This strategy assumes that measurement scans and correction scan points share
the *same sequential order*, which is usually true when samples are measured in
a fixed sequence at the beamline.

Data-driven concentration correction
--------------------------------------

The :class:`~daxs.correctors.DataDrivenConcentrationCorrector` is activated by
additionally supplying a ``data_mappings`` dictionary:

.. code-block:: python

    data_mappings = {
        "sy":    ".1/instrument/positioners/sy",
        "owisz": ".1/instrument/positioners/owisz",
    }
    measurement.concentration_correction(indices=225, data_mappings=data_mappings)

Here ``data_mappings`` names one or more *positioner counters*.  For each
measurement scan, ``daxs`` reads the positioner values that were recorded *at
the time of that scan*.  It then searches the correction scan for the point
whose positioner values are closest in Euclidean distance and uses that point
as the correction factor.

This approach is more robust because it does not rely on scan order.  As long
as the sample returns to the same position (within numerical precision),
the matching succeeds — even if the order in which positions were visited
differs between the measurement and the correction scan.

.. note::
   The keys in ``data_mappings`` must correspond to fields that were recorded
   in the *measurement* scans (as additional entries in the source
   ``data_mappings``).  The values must be paths to the equivalent counter
   columns in the *correction* scan.  See also the
   :doc:`../examples/advanced_concentration_correction` example.

Supplying pre-built scan objects
---------------------------------

Both correctors also accept pre-built :class:`~daxs.scans.Scan` or
:class:`~daxs.scans.Scans` objects via the ``scans`` argument, which is
useful when the correction data lives in a different file:

.. code-block:: python

    from daxs.sources import Hdf5Source

    corr_source = Hdf5Source(other_file, selection=42, data_mappings={...})
    measurement.concentration_correction(scans=corr_source.scans)

API reference
-------------

See :meth:`~daxs.measurements.Measurement.concentration_correction`,
:class:`~daxs.correctors.SimpleConcentrationCorrector`, and
:class:`~daxs.correctors.DataDrivenConcentrationCorrector`.
