diff --git a/src/toolbox_scs/misc/laser_utils.py b/src/toolbox_scs/misc/laser_utils.py
index b284999e94af2921cdd4313f5be08bd996a335d5..e4f5dc8a672396d7f51a305da6cbad33ae3c4775 100644
--- a/src/toolbox_scs/misc/laser_utils.py
+++ b/src/toolbox_scs/misc/laser_utils.py
@@ -1,8 +1,11 @@
 __all__ = [
     'degToRelPower',
     'positionToDelay',
+    'fluenceCalibration',
 ]
 
+import numpy as np
+import matplotlib.pyplot as plt
 
 def positionToDelay(pos, origin=0, invert = False, reflections=1):
     ''' converts a motor position in mm into optical delay in picosecond
@@ -30,3 +33,82 @@ def degToRelPower(x, theta0=0):
             array-like relative power
     '''
     return np.sin(2*(x-theta0)*np.pi/180)**2
+
+def fluenceCalibration(hwp, power_mW, npulses, w0x, w0y=None,
+                       train_rep_rate=10, fit_order=1,
+                       plot=True, xlabel='HWP [%]'):
+    """
+    Given a series of relative powers or half wave plate angles and
+    averaged powers in mW, this routine calculates the corresponding
+    fluence and fits a polynomial to the data.
+    Parameters
+    ----------
+    hwp: array-like (N)
+        angle or relative power from the half wave plate
+    power_mW: array-like (N)
+        measured power in mW by powermeter
+    npulses: int
+        number of pulses per train during power measurement
+    w0x: float
+        radius at 1/e^2 in x-axis
+    w0y: float, optional
+        radius at 1/e^2 in y-axis. If None, w0y=w0x is assumed.
+    train_rep_rate: float
+        repetition rate of the FEL, by default equals to 10 Hz.
+    fit_order: int
+        order of the polynomial fit
+    plot: bool
+        Plot the results if True
+    xlabel: str
+        xlabel for the plot
+
+    Output
+    ------
+    F: ndarray (N)
+        fluence in mJ/cm^2
+    fit: ndarray
+        coefficients of the polynomial fit
+    """
+    power = np.array(power_mW)
+    hwp = np.array(hwp)
+    E = power/(train_rep_rate*npulses)*1e-3 # pulse energy in J
+    if w0y is None:
+        w0y = w0x
+    F = 2*E/(np.pi*w0x*w0y) # fluence in J/m^2
+    
+    fit_E = np.polyfit(hwp, E*1e6, fit_order)
+    fit_F = np.polyfit(hwp, F*1e-1, fit_order)
+    x = np.linspace(hwp.min(), hwp.max(), 100)
+    if plot:
+        plt.figure(figsize=(7,4))
+        ax = plt.subplot(121)
+        plt.title(f'w0x = {w0x*1e6:.0f} $\mu$m, w0y = {w0y*1e6:.0f} $\mu$m')
+        plt.plot(hwp, E*1e6, 'o', label='data')
+        fit_label=''
+        for i in range(len(fit_E)-1, 1, -1):
+            fit_label += f'{fit_E[i]:.2g}x$^{i}$ + '
+            if i%2 == 0:
+                fit_label +='\n'
+        fit_label += f'{fit_E[-2]:.2g}x + {fit_E[-1]:.2g}'
+        plt.plot(x, np.poly1d(fit_E)(x), label=fit_label)
+        plt.ylabel('Pulse energy [$\mu$J]')
+        plt.xlabel('HWP [%]')
+        plt.legend()
+        plt.grid()
+        plt.subplot(122, sharex=ax)
+        plt.title(f'w0x = {w0x*1e6:.0f} $\mu$m, w0y = {w0y*1e6:.0f} $\mu$m')
+        plt.plot(hwp, F*1e-1, 'o', label='data')
+        fit_label=''
+        for i in range(len(fit_F)-1, 1, -1):
+            fit_label += f'{fit_F[i]:.2g}x$^{i}$ + '
+            if i%2 == 0:
+                fit_label +='\n'
+        fit_label += f'{fit_F[-2]:.2g}x + {fit_F[-1]:.2g}'
+        #fit_label = '+'.join([f'{f:.2g}x$^{i}$' for i, f in enumerate(fit_F[:-1])])
+        plt.plot(x, np.poly1d(fit_F)(x), label=fit_label)
+        plt.ylabel('Fluence [mJ/cm$^2$]')
+        plt.xlabel('HWP [%]')
+        plt.legend()
+        plt.grid()
+        plt.tight_layout()
+    return F*1e-1, fit_F, E*1e6, fit_E
\ No newline at end of file