diff --git a/src/toolbox_scs/detectors/viking.py b/src/toolbox_scs/detectors/viking.py
new file mode 100644
index 0000000000000000000000000000000000000000..36324014d33532bfb8f848b777b4693168384211
--- /dev/null
+++ b/src/toolbox_scs/detectors/viking.py
@@ -0,0 +1,241 @@
+import numpy as np
+import xarray as xr
+import toolbox_scs as tb
+import matplotlib.pyplot as plt
+
+__all__ = ['Viking']
+
+# -----------------------------------------------------------------------------
+# Viking class
+class Viking:
+    # run
+    PROPOSAL = 2953
+
+    # image range
+    X_RANGE = slice(None, None)
+    Y_RANGE = slice(None, None)
+    
+    # dimension for integration
+    INTEGRATE_DIM = 'newt_y'
+    USE_DARK = False
+    
+    # polynomial degree for background subtraction
+    POLY_DEG = 5
+
+    FIELDS = ['newton']
+    
+    ENERGY_CALIB = [9.8233e-7, 0.0240, 514.4795]
+
+    def set_params(self, **params):
+        for key, value in params.items():
+            setattr(self, key.upper(), value)
+
+    def get_params(self, *params):
+        if not params:
+            params = ('proposal', 'x_range', 'y_range', 'integrate_dim',
+                      'fields',)
+        return {param: getattr(self, param.upper()) for param in params}
+
+    def from_run(self, runNB, proposal=None, add_attrs=True, calibrate=True):
+        if proposal is None:
+            proposal = self.PROPOSAL
+        roi = {'newton': {'newton': {'roi': (self.Y_RANGE, self.X_RANGE),
+                                      'dim': ['newt_y', 'newt_x']}}}
+        run, data = tb.load(proposal, runNB=runNB,
+                            fields=self.FIELDS, rois=roi)
+        data['newton'] = data['newton'].astype(float)
+        if calibrate:
+            data = data.assign_coords(newt_x=np.polyval(self.ENERGY_CALIB,
+                                                        data['newt_x']))
+        if add_attrs:
+            params = self.get_camera_params(run)
+            for k, v in params.items():
+                data.attrs[k] = v
+        return data
+
+    def load_dark(self, runNB=None, proposal=None):
+        if runNB is None:
+            self.USE_DARK = False
+            return
+        if proposal is None:
+            proposal = self.PROPOSAL
+        data = self.from_run(runNB, proposal, add_attrs=False)
+        self.dark_image = data['newton'].mean(dim='trainId')
+        self.dark_image.attrs['runNB'] = runNB
+        self.USE_DARK = True
+
+    def integrate(self, data):
+        imgs = data['newton']
+        if self.USE_DARK:
+            imgs -= self.dark_image
+        data['spectrum'] = imgs.sum(dim=self.INTEGRATE_DIM)
+        return data
+    
+    def get_camera_gain(self, run):
+        """
+        Get the preamp gain of the camera in the Viking spectrometer for
+        a specified run.
+
+        Parameters:
+        ------
+        run: extra_data DataCollection
+            information on the run
+
+        Output:
+        ------
+        gain: int
+        """
+        gain = run.get_run_value('SCS_EXP_NEWTON/CAM/CAMERA',
+                                 'preampGain.value')
+        gain_dict = {0: 1, 1: 2, 2: 4}
+        return gain_dict[gain]
+
+    def get_electrons_per_counts(self, run, gain=None):
+        """
+        Conversion factor from camera digital counts to photoelectrons
+        per count. The values can be found in the camera datasheet
+        but they have been slightly corrected for High Sensitivity
+        mode after analysis of runs 1204, 1207 and 1208, proposal 2937.
+
+        Parameters:
+        ------
+            run: extra_data DataCollection
+                information on the run
+            gain: int
+                the camera preamp gain
+
+        Outputs:
+        ------
+        ret: float
+            photoelectrons per count
+        """
+        if gain is None:
+            gain = self.get_camera_gain(run)
+        hc = run.get_run_value('SCS_EXP_NEWTON/CAM/CAMERA',
+                               'HighCapacity.value')
+        if hc == 0: # High Sensitivity
+            pe_dict = {1: 4., 2: 2.05, 4: 0.97}
+        elif hc == 1: # High Capacity
+            pe_dict = {1: 17.9, 2: 9., 4: 4.5}
+        return pe_dict[gain]
+
+    def get_camera_params(self, run):
+        dic = {'vbin:': 'imageSpecifications.verticalBinning.value',
+               'hbin': 'imageSpecifications.horizontalBinning.value',
+               'startX': 'imageSpecifications.startX.value',
+               'endX': 'imageSpecifications.endX.value',
+               'startY': 'imageSpecifications.startY.value',
+               'endY': 'imageSpecifications.endY.value',
+               'temperature': 'CoolerActual.temperature.value',
+               'high_sensitivity': 'HighCapacity.value',
+               'exposure_s': 'exposureTime.value'
+              }
+        ret = {}
+        for k, v in dic.items():
+            ret[k] = run.get_run_value('SCS_EXP_NEWTON/CAM/CAMERA', v)
+        ret['gain'] = self.get_camera_gain(run)
+        ret['photoelectrons_per_count'] = self.get_electrons_per_counts(run, ret['gain'])
+        return ret
+
+    def removePolyBaseline(self, data, signalRange=[515, 540]):
+        """
+        Removes a polynomial baseline to a signal, assuming a fixed
+        position for the signal.
+        Parameters
+        ----------
+        x: array-like, shape(M,)
+            x-axis
+        spectra: array-like, shape(M,) or (N, M,)
+            the signals to subtract a baseline from. If 2d, the signals
+            are assumed to be stacked on the first axis.
+        deg: int
+            the polynomial degree for fitting a baseline
+        signalRange: list of type(x), length 2
+            the x-interval where to expect the signal. The baseline is fitted to
+            all regions except the one defined by the interval.
+        Output
+        ------
+        spectra_nobl: array-like, shape(M,) or (N, M,)
+            the baseline subtracted spectra
+
+        """
+        if 'spectrum' not in data:
+            return
+        x = data.newt_x
+        spectra = data['spectrum']
+        mask = (x<signalRange[0]) | (x>signalRange[1])
+        if isinstance(x, xr.DataArray):
+            x_bl = x.where(mask, drop=True)
+            bl = spectra.sel(newt_x=x_bl)
+        else:
+            x_bl = x[mask]
+            if len(spectra.shape) == 1:
+                bl = spectra[mask]
+            else:
+                bl = spectra[:, mask]
+        fit = np.polyfit(x_bl, bl.T, self.POLY_DEG)
+        if len(spectra.shape) == 1:
+            return spectra - np.poly1d(fit)(x)
+        final_bl = np.empty(spectra.shape)
+        for t in range(spectra.shape[0]):
+            final_bl[t] = np.poly1d(fit[:, t])(x)
+        data['spectrum_nobg'] = spectra - final_bl
+        return spectra - final_bl
+
+    def xas(self, data, data_ref, thickness=1, plot=False,
+            plot_errors=True, xas_ylim=(-1, 3)):
+        key = 'spectrum_nobg' if 'spectrum_nobg' in data else 'spectrum'
+        if data['newt_x'].equals(data_ref['newt_x']) is False:
+            return 
+        spectrum = data[key].mean(dim='trainId')
+        std = data[key].std(dim='trainId')
+        std_err = std / np.sqrt(data.sizes['trainId'])
+        spectrum_ref = data_ref[key].mean(dim='trainId')
+        std_ref = data_ref[key].std(dim='trainId')
+        std_err_ref = std_ref / np.sqrt(data_ref.sizes['trainId'])
+        
+        ds = xr.Dataset()
+        ds['sample'] = spectrum
+        ds['sample_std'] = std
+        ds['sample_std_err'] = std_err
+        ds['ref'] = spectrum_ref
+        ds['ref_std'] = std
+        ds['ref_std_err'] = std_err
+        ds['absorption'] = spectrum_ref / spectrum
+        ds['absorption_std'] = np.abs(ds['absorption']) * np.sqrt(
+            std_ref**2 / spectrum_ref**2 + std**2 / spectrum**2)
+        ds['absorption_stderr'] = np.abs(ds['absorption']) * np.sqrt(
+            (std_err_ref / spectrum_ref)**2 + (std_err / spectrum)**2)
+        ds['absorptionCoef'] = np.log(ds['absorption']) / thickness
+        ds['absorptionCoef_std'] = ds['absorption_std'] / (thickness * np.abs(ds['absorption']))
+        ds['absorptionCoef_stderr'] = ds['absorption_stderr'] / (thickness * np.abs(ds['absorption']))
+        
+        if plot:
+            plot_viking_xas(ds, plot_errors, xas_ylim)
+        return ds
+
+def plot_viking_xas(xas, plot_errors=True, xas_ylim=(-1, 3)):
+    fig, ax = plt.subplots(3, 1, figsize=(8,8), sharex=True)
+    ax[0].plot(xas.newt_x, xas['ref'])
+    ax[0].grid()
+
+    ax[1].plot(xas.newt_x, xas['sample'])
+    ax[1].grid()
+
+    ax[2].plot(xas.newt_x, xas['absorptionCoef'])
+    ax[2].set_ylim(*xas_ylim)
+    ax[2].grid()
+    
+    if plot_errors:
+        ax[0].fill_between(xas.newt_x,
+                           xas['ref'] - 1.96*xas['ref_std_err'], 
+                           xas['ref'] + 1.96*xas['ref_std_err'],
+                           alpha=0.2)
+        ax[1].fill_between(xas.newt_x,
+                           xas['sample'] - 1.96*xas['sample_std_err'], 
+                           xas['sample'] + 1.96*xas['sample_std_err'],
+                           alpha=0.2)
+        ax[2].fill_between(xas.newt_x,
+                           xas['absorptionCoef'] - 1.96*xas['absorptionCoef_stderr'], 
+                           xas['absorptionCoef'] + 1.96*xas['absorptionCoef_stderr'],
+                           alpha=0.2)