diff --git a/XAS.py b/XAS.py
new file mode 100644
index 0000000000000000000000000000000000000000..092fe4f1b243d3857602d04217505a9dcdcfc0af
--- /dev/null
+++ b/XAS.py
@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+""" Toolbox for XAS experiments.
+
+    Based on the LCLS LO59 experiment libraries.
+
+    Time-resolved XAS and XMCD with uncertainties.
+
+    Copyright (2019-) SCS Team
+    Copyright (2017-2019) Loïc Le Guyader <loic.le.guyader@xfel.eu>
+"""
+
+import numpy as np
+
+def absorption(T, Io):
+    """ Compute the absorption A = -ln(T/Io)
+        Inputs:
+            T: 1-D transmission value array of length N
+            Io: 1-D Io monitor value array of length N
+
+        Output:
+            a structured array with:
+                muA: absorption mean
+                sigmaA: absorption standard deviation
+                weights: sum of Io values
+                muT: transmission mean
+                sigmaT: transmission standard deviation
+                muIo: Io mean
+                sigmaIo: Io standard deviation
+                p: correlation coefficient between T and Io
+                counts: length of T
+    """
+
+    counts = len(T)
+    assert counts == len(Io), "T and Io must have the same length"
+
+    # return type of the structured array
+    fdtype = [('muA', 'f8'), ('sigmaA', 'f8'), ('weights', 'f8'),
+        ('muT', 'f8'), ('sigmaT', 'f8'), ('muIo', 'f8'), ('sigmaIo', 'f8'),
+        ('p', 'f8'), ('counts', 'i8')]
+    if counts == 0:
+        return np.array([(np.NaN, np.NaN, np.NaN, np.NaN, np.NaN, np.NaN,
+            np.NaN, np.NaN, 0)], dtype=fdtype)
+
+    muT = np.mean(T)
+    sigmaT = np.std(T)
+
+    muIo = np.mean(Io)
+    sigmaIo = np.std(Io)
+    weights = np.sum(Io)
+
+    p = np.corrcoef(T, Io)[0,1]
+
+    muA = -np.log(muT/muIo)
+    # from error propagation for correlated data
+    sigmaA = (np.sqrt((sigmaT/muT)**2 + (sigmaIo/muIo)**2
+        - 2*p*sigmaIo*sigmaT/(muIo*muT)))
+    
+    # direct calculation
+    #mask = (Io != 0)
+    #sigmaA = np.nanstd(-np.log(T[mask]/Io[mask]))
+
+    return np.array([(muA, sigmaA, weights, muT, sigmaT, muIo, sigmaIo,
+        p, counts)], dtype=fdtype)
+
+def binning(x, data, func, bins=100, bin_length=None):
+    """ General purpose 1-dimension data binning
+
+        Inputs:
+            x: input vector of len N
+            data: structured array of len N
+            func: a function handle that takes data from a bin an return
+                a structured array of statistics computed in that bin
+            bins: array of bin-edges or number of desired bins
+            bin_length: if not None, the bin width covering the whole range
+
+        Outputs:
+            bins: the bins edges
+            res: a structured array of binned data
+    """
+
+    if bin_length is not None:
+        bin_start = np.amin(x)
+        bin_end = np.amax(x)
+        bins = np.arange(bin_start, bin_end+bin_length, bin_length)
+    elif np.size(bins) == 1:
+        bin_start = np.amin(x)
+        bin_end = np.amax(x)
+        bins = np.linspace(bin_start, bin_end, bins)
+    bin_centers = (bins[1:]+bins[:-1])/2
+    nb_bins = len(bin_centers)
+
+    bin_idx = np.digitize(x, bins)
+    dummy = func([])
+    res = np.empty((nb_bins), dtype=dummy.dtype)
+    for k in range(nb_bins):
+        res[k] = func(data[bin_idx == k])
+
+    return bins, res
+
+def xas(nrun, bins=None, Iokey='SCS_XGM', Itkey='MCP3apd', nrjkey='nrj', Iooffset=0):
+    """ Compute the XAS spectra from a xarray nrun.
+
+        Inputs:
+            nrun: xarray of SCS data
+            bins: array of bin-edges or number of desired bins
+            Iokey: string for the Io fields, typically 'SCS_XGM'
+            Itkey: string for the It fields, typically 'MCP3apd'
+            NRJkey: string for the nrj fields, typically 'nrj'
+            Iooffset: offset to apply on Io
+
+        Outputs:
+            a dictionnary containing:
+                nrj: the bins center
+                muA: the absorption
+                sigmaA: standard deviation on the absorption
+                sterrA: standard error on the absorption
+                muIo: the mean of the Io
+                counts: the number of events in each bin
+    """
+    
+    stacked = nrun.stack(x=['trainId','pId'])
+
+    Io = stacked[Iokey].values + Iooffset
+    It = stacked[Itkey].values
+    nrj = stacked[nrjkey].values
+    
+    names_list = ['nrj', 'Io', 'It']
+    rundata = np.vstack((nrj, Io, It))
+    rundata = np.rec.fromarrays(rundata, names=names_list)
+    
+    def whichIo(data):
+        """ Select which fields to use as I0 and which to use as I1
+        """
+        if len(data) == 0:
+            return absorption([], [])
+        else:
+            return absorption(-data['It'], data['Io'])
+
+    if bins is None:
+        num_bins = 80
+        energy_limits = [np.min(nrj), np.max(nrj)]
+        bins = np.linspace(energy_limits[0], energy_limits[1], num_bins+1)
+        
+    dummy, nosample = binning(rundata['nrj'], rundata, whichIo, bins)
+    muA = nosample['muA']
+    sterrA = nosample['sigmaA']/np.sqrt(nosample['counts'])
+
+    bins_c = 0.5*(bins[1:] + bins[:-1])
+    
+    return {'nrj':bins_c, 'muA':muA, 'sterrA':sterrA, 'sigmaA':nosample['sigmaA'],
+        'muIo':nosample['muIo'], 'counts':nosample['counts']}
+
+def xasxmcd(dataP, dataN):
+    """ Compute XAS and XMCD from data with both magnetic field direction
+        Inputs:
+            dataP: structured array for positive field
+            dataN: structured array for negative field
+
+        Outputs:
+            xas: structured array for the sum
+            xmcd: structured array for the difference
+    """
+
+    assert len(dataP) == len(dataN), "binned datasets must be of same lengths"
+    assert not np.any(dataP['nrj'] - dataN['nrj']), "Energy points for dataP and dataN should be the same"
+
+    muXAS = dataP['muA'] + dataN['muA']
+    muXMCD = dataP['muA'] - dataN['muA']
+
+    # standard error is the same for XAS and XMCD
+    sigma = np.sqrt(dataP['sterrA']**2 + dataN['sterrA']**2)
+
+    res = np.empty(len(muXAS), dtype=[('nrj', 'f8'), ('muXAS', 'f8'), ('sigmaXAS', 'f8'),
+        ('muXMCD', 'f8'), ('sigmaXMCD', 'f8')])
+    res['nrj'] = dataP['nrj']
+    res['muXAS'] = muXAS
+    res['muXMCD'] = muXMCD
+    res['sigmaXAS'] = sigma
+    res['sigmaXMCD'] = sigma
+
+    return res
diff --git a/__init__.py b/__init__.py
index e13d7ef6f190a600520c26bbfc062077e2b1f019..23e93fcfd21dab40e2ef44c354b699046e57c4bb 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,2 +1,3 @@
 from ToolBox.Load import *
 from ToolBox.xgm import *
+from ToolBox.XAS import *