From 414dd79647acdd6313a7027ffe7140b02958d09b Mon Sep 17 00:00:00 2001
From: Marko Mecina <marko.mecina@univie.ac.at>
Date: Mon, 19 Jun 2023 17:09:46 +0200
Subject: [PATCH] add module providing calibration functions for SMILE

---
 Ccs/calibrations_SMILE.py | 468 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 468 insertions(+)
 create mode 100644 Ccs/calibrations_SMILE.py

diff --git a/Ccs/calibrations_SMILE.py b/Ccs/calibrations_SMILE.py
new file mode 100644
index 0000000..304ef7f
--- /dev/null
+++ b/Ccs/calibrations_SMILE.py
@@ -0,0 +1,468 @@
+"""
+Calibration functions and utilities for raw/engineering conversions in SMILE
+
+Data from SMILE-IWF-PL-UM-147-d0-3_SXI_EBox_User_Manual (ID 5233)
+"""
+
+import os
+import numpy as np
+import scipy as sp
+import matplotlib.pyplot as plt
+
+# constants
+T_ZERO = 273.15
+
+# common ADC coefficients
+ADC_INPRNG = 7.34783  # V
+ADC_OFFSET = -1.69565  # V
+
+
+class Dpu:
+    ADC_P3V9 = "HK_ADC_P3V9"
+    ADC_P3V3 = "HK_ADC_P3V3"
+    ADC_P3V3_LVDS = "HK_ADC_P3V3_LVDS"
+    ADC_P2V5 = "HK_ADC_P2V5"
+    ADC_P1V8 = "HK_ADC_P1V8"
+    ADC_P1V2 = "HK_ADC_P1V2"
+    ADC_REF = "HK_ADC_REF"
+
+
+K_DPU = {
+    Dpu.ADC_P3V9: 2,
+    Dpu.ADC_P3V3: 1,
+    Dpu.ADC_P3V3_LVDS: 1,
+    Dpu.ADC_P2V5: 1,
+    Dpu.ADC_P1V8: 1,
+    Dpu.ADC_P1V2: 1,
+    Dpu.ADC_REF: 1
+}
+
+
+class Temp:
+    ADC_TEMP1 = "HK_ADC_TEMP1"
+    ADC_TEMP_FEE = "HK_ADC_TEMP_FEE"
+    ADC_TEMP_CCD = "HK_ADC_TEMP_CCD"
+    ADC_PSU_TEMP = "HK_ADC_PSU_TEMP"
+
+
+# Signal specific coefficients
+class V_T0:
+    CCD = 2.5650
+    TEMP1 = 2.5770
+    FEE = 1.2800
+
+
+class K_T:
+    CCD = 0.00385
+    TEMP1 = 0.00385
+    FEE = 0.00385
+
+
+# interpolation table for nominal operation CCD temperature
+# (degC, ADC_V, ADU_dec, ADU_hex)
+CCD_TEMP_TABLE = [
+    (-140.0, 1.125, 6288, 0x1890),
+    (-135.0, 1.178, 6407, 0x1906),
+    (-130.0, 1.231, 6524, 0x197C),
+    (-125.0, 1.283, 6642, 0x19F1),
+    (-120.0, 1.336, 6759, 0x1A66),
+    (-115.0, 1.388, 6876, 0x1ADB),
+    (-110.0, 1.440, 6992, 0x1B50),
+    (-105.0, 1.493, 7109, 0x1BC4),
+    (-100.0, 1.545, 7225, 0x1C38),
+    (-95.0, 1.596, 7340, 0x1CAC),
+    (-90.0, 1.648, 7456, 0x1D1F),
+    (-85.0, 1.700, 7571, 0x1D92),
+    (-80.0, 1.751, 7686, 0x1E05),
+    (-75.0, 1.803, 7800, 0x1E78),
+    (-70.0, 1.854, 7915, 0x1EEA),
+    (-65.0, 1.905, 8029, 0x1F5D),
+    (-60.0, 1.957, 8143, 0x1FCF),
+    (-55.0, 2.008, 8257, 0x2040),
+    (-50.0, 2.059, 8371, 0x20B2),
+    (-45.0, 2.109, 8484, 0x2123),
+    (-40.0, 2.160, 8597, 0x2195),
+    (-35.0, 2.211, 8710, 0x2206),
+    (-30.0, 2.261, 8823, 0x2276),
+    (-25.0, 2.312, 8936, 0x22E7),
+    (-20.0, 2.362, 9048, 0x2358)
+]
+
+# interpolation table for PSU temperature
+# (degC, ADC_V, ADU_dec, ADU_hex)
+PSU_TEMP = [
+    (-50.0, 3.237, 10998, 0x2AF6),
+    (-40.0, 3.187, 10887, 0x2A86),
+    (-20.0, 2.960, 10380, 0x288C),
+    (0.0, 2.487, 9326, 0x246D),
+    (20.0, 1.816, 7830, 0x1E95),
+    (25.0, 1.643, 7444, 0x1D13),
+    (40.0, 1.169, 6387, 0x18F3),
+    (60.0, 0.703, 5348, 0x14E4),
+    (80.0, 0.417, 4710, 0x1266),
+    (90.0, 0.323, 4501, 0x1194),
+    (100.0, 0.252, 4343, 0x10F6)
+]
+
+
+class Psu:
+    ADC_I_FEE_ANA = "HK_ADC_I_FEE_ANA"
+    ADC_I_FEE_DIG = "HK_ADC_I_FEE_DIG"
+    ADC_I_DPU = "HK_ADC_I_DPU"
+    ADC_I_RSE = "HK_ADC_I_RSE"
+    ADC_I_HEATER = "HK_ADC_I_HEATER"
+
+
+K_PSU = {
+    Psu.ADC_I_FEE_ANA: 0.3058,
+    Psu.ADC_I_FEE_DIG: 0.1528,
+    Psu.ADC_I_DPU: 0.4913,
+    Psu.ADC_I_RSE: 0.844,
+    Psu.ADC_I_HEATER: 0.4349
+}
+
+PSU_OFFSET = {
+    Psu.ADC_I_FEE_ANA: 0,
+    Psu.ADC_I_FEE_DIG: 0,
+    Psu.ADC_I_DPU: 0,
+    Psu.ADC_I_RSE: 0,
+    Psu.ADC_I_HEATER: -0.3701
+}
+
+
+class Rse:
+    RSE_MOTOR_TEMP = "HK_RSE_MOTOR_TEMP"
+    RSE_ELEC_TEMP = "HK_RSE_ELEC_TEMP"
+
+
+# fit polynomial of degree POLY_DEG through CCD ADU-degC relation (operational range)
+_ccd_temp_adu_array = np.array(CCD_TEMP_TABLE).T  # (degC, ADC_V, ADU_dec, ADU_hex)
+POLY_DEG = 4
+_ccd_temp_fit_adu = np.polynomial.polynomial.Polynomial.fit(_ccd_temp_adu_array[2], _ccd_temp_adu_array[0],
+                                                            POLY_DEG).convert()
+_ccd_temp_fit_adu_inv = np.polynomial.polynomial.Polynomial.fit(_ccd_temp_adu_array[0], _ccd_temp_adu_array[2],
+                                                                POLY_DEG).convert()
+
+# cubic-spline interpolation of PSU ADU-degC relation (nominal values)
+_psu_temp_adu_array = np.array(PSU_TEMP).T  # (degC, ADC_V, ADU_dec, ADU_hex)
+_psu_temp_interp = sp.interpolate.interp1d(_psu_temp_adu_array[2], _psu_temp_adu_array[0],
+                                           kind='cubic', fill_value='extrapolate')
+_psu_temp_interp_inv = sp.interpolate.interp1d(_psu_temp_adu_array[0], _psu_temp_adu_array[2], kind='cubic',
+                                               fill_value='extrapolate')
+
+
+def t_ccd_adu_to_deg_oper(adu, warn=True):
+    if not ((_ccd_temp_adu_array[2].min() <= adu) & (adu <= _ccd_temp_adu_array[2].max())).all() and warn:
+        print('WARNING! Value(s) outside operational range ({:.0f}-{:.0f})!'.format(_ccd_temp_adu_array[2].min(),
+                                                                                    _ccd_temp_adu_array[2].max()))
+    return _ccd_temp_fit_adu(adu)
+
+
+def t_ccd_deg_to_adu_oper(t, warn=True):
+    if not ((_ccd_temp_adu_array[0].min() <= t) & (t <= _ccd_temp_adu_array[0].max())).all() and warn:
+        print('WARNING! Value(s) outside operational range ({} - {})!'.format(_ccd_temp_adu_array[0].min(),
+                                                                              _ccd_temp_adu_array[0].max()))
+    return round(_ccd_temp_fit_adu_inv(t))
+
+
+def t_ccd_adu_to_deg_nonoper(adu):
+    return (adu * ADC_INPRNG / (2 ** 14 - 1) + ADC_OFFSET - V_T0.CCD) / (V_T0.CCD * K_T.CCD)
+
+
+def t_ccd_deg_to_adu_nonoper(t):
+    return round(((t * V_T0.CCD * K_T.CCD - ADC_OFFSET + V_T0.CCD) * (2 ** 14 - 1)) / ADC_INPRNG)
+
+
+def t_ccd_adu_to_deg(adu):
+    return np.where(adu <= _ccd_temp_adu_array[2].max(), t_ccd_adu_to_deg_oper(adu, warn=False), t_ccd_adu_to_deg_nonoper(adu))
+
+
+def t_ccd_deg_to_adu(t):
+    return np.where(t <= _ccd_temp_adu_array[0].max(), t_ccd_deg_to_adu_oper(t, warn=False), t_ccd_deg_to_adu_nonoper(t))
+
+
+def t_ccd_fee_adu_to_deg(adu):
+    """
+    For CCD temperature reported in FEE HK
+
+    :param adu:
+    :return:
+    """
+    return adu / 65535 * 4.096 * 338.581 - T_ZERO
+
+
+def t_ccd_fee_deg_to_adu(t):
+    """
+    For CCD temperature reported in FEE HK
+
+    :param t:
+    :return:
+    """
+    return round((t + T_ZERO) / (4.096 * 338.581) * 65535)
+
+
+def t_temp1_adu_to_deg(adu):
+    return (adu * ADC_INPRNG / (2 ** 14 - 1) + ADC_OFFSET - V_T0.TEMP1) / (V_T0.TEMP1 * K_T.TEMP1)
+
+
+def t_temp1_deg_to_adu(t):
+    return round(((t * V_T0.TEMP1 * K_T.TEMP1 - ADC_OFFSET + V_T0.TEMP1) * (2 ** 14 - 1)) / ADC_INPRNG)
+
+
+def t_fee_adu_to_deg(adu):
+    return (adu * ADC_INPRNG / (2 ** 14 - 1) + ADC_OFFSET - V_T0.FEE) / (V_T0.FEE * K_T.FEE)
+
+
+def t_fee_deg_to_adu(t):
+    return round(((t * V_T0.FEE * K_T.FEE - ADC_OFFSET + V_T0.FEE) * (2 ** 14 - 1)) / ADC_INPRNG)
+
+
+def t_rse_adu_to_deg(adu):
+    return (3.908 - np.sqrt(17.59246 - (76.56 / (4096 / adu - 1)))) / 0.00116
+
+
+def t_rse_deg_to_adu(t):
+    return round(4096 / (76.56 / (17.59246 - (3.908 - 0.00116 * t) ** 2) + 1))
+
+
+def t_psu_adu_to_deg(adu):
+    return _psu_temp_interp(adu)
+
+
+def t_psu_deg_to_adu(t):
+    return _psu_temp_interp_inv(t)
+
+
+def t_adu_to_deg(adu, signal):
+    if signal == Temp.ADC_TEMP_CCD:
+        t = t_ccd_adu_to_deg(adu)
+    elif signal == Temp.ADC_TEMP1:
+        t = t_temp1_adu_to_deg(adu)
+    elif signal == Temp.ADC_TEMP_FEE:
+        t = t_fee_adu_to_deg(adu)
+    elif signal == Temp.ADC_PSU_TEMP:
+        t = t_psu_adu_to_deg(adu)
+    elif signal in (Rse.RSE_MOTOR_TEMP, Rse.RSE_ELEC_TEMP):
+        t = t_rse_adu_to_deg(adu)
+    else:
+        raise ValueError("Unknown signal '{}'".format(signal))
+
+    return t
+
+
+def t_deg_to_adu(t, signal):
+    if signal == Temp.ADC_TEMP_CCD:
+        adu = t_ccd_deg_to_adu(t)
+    elif signal == Temp.ADC_TEMP1:
+        adu = t_temp1_deg_to_adu(t)
+    elif signal == Temp.ADC_TEMP_FEE:
+        adu = t_fee_deg_to_adu(t)
+    elif signal == Temp.ADC_PSU_TEMP:
+        adu = t_psu_deg_to_adu(t)
+    elif signal in (Rse.RSE_MOTOR_TEMP, Rse.RSE_ELEC_TEMP):
+        adu = t_rse_deg_to_adu(t)
+    else:
+        raise ValueError("Unknown signal '{}'".format(signal))
+
+    return adu
+
+
+def u_dpu_adu_to_volt(adu, signal):
+    return ((adu * ADC_INPRNG) / (2 ** 14 - 1) + ADC_OFFSET) * K_DPU[signal]
+
+
+def u_dpu_volt_to_adu(u, signal):
+    return round(((u / K_DPU[signal] - ADC_OFFSET) * (2 ** 14 - 1)) / ADC_INPRNG)
+
+
+def i_psu_adu_to_amp(adu, signal):
+    return ((adu * ADC_INPRNG) / (2 ** 14 - 1) + ADC_OFFSET) * K_PSU[signal] + PSU_OFFSET[signal]
+
+
+def i_psu_amp_to_adu(i, signal):
+    return round((((i - PSU_OFFSET[signal]) / K_PSU[signal] - ADC_OFFSET) * (2 ** 14 - 1)) / ADC_INPRNG)
+
+
+def calibrate(adu, signal):
+    if signal in Dpu.__dict__.values():
+        x = u_dpu_adu_to_volt(adu, signal)
+    elif signal in Temp.__dict__.values() or signal in Rse.__dict__.values():
+        x = t_adu_to_deg(adu, signal)
+    elif signal in Psu.__dict__.values():
+        x = i_psu_adu_to_amp(adu, signal)
+    else:
+        raise ValueError("Unknown signal '{}'".format(signal))
+
+    return x
+
+
+def decalibrate(x, signal):
+    if signal in Dpu.__dict__.values():
+        adu = u_dpu_volt_to_adu(x, signal)
+    elif signal in Temp.__dict__.values() or signal in Rse.__dict__.values():
+        adu = t_deg_to_adu(x, signal)
+    elif signal in Psu.__dict__.values():
+        adu = i_psu_amp_to_adu(x, signal)
+    else:
+        raise ValueError("Unknown signal '{}'".format(signal))
+
+    return adu
+
+
+class CalibrationTables:
+    # default ADC limits
+    BOUND_L = 0
+    BOUND_U = 0x3FFE
+
+    BOUND_RSE = 0xCD
+
+    def __init__(self):
+
+        # temperatures
+        x = np.linspace(self.BOUND_L, self.BOUND_U, 60, dtype=int)
+        self.temperature = {}
+        for sig in vars(Temp):
+            if sig.startswith('ADC'):
+                label = getattr(Temp, sig)
+                self.temperature[label] = np.array([x, t_adu_to_deg(x, label)])
+
+        x = np.linspace(self.BOUND_L, self.BOUND_RSE, 60, dtype=int)
+        for sig in vars(Rse):
+            if sig.startswith('RSE'):
+                label = getattr(Rse, sig)
+                self.temperature[label] = np.array([x, t_adu_to_deg(x, label)])
+
+        x = np.linspace(self.BOUND_L, self.BOUND_U, 2, dtype=int)  # two points suffice for linear voltage and current calibrations
+        # voltages
+        self.voltage = {}
+        for sig in vars(Dpu):
+            if sig.startswith('ADC'):
+                label = getattr(Dpu, sig)
+                self.voltage[label] = np.array([x, u_dpu_adu_to_volt(x, label)])
+
+        # currents
+        self.current = {}
+        for sig in vars(Psu):
+            if sig.startswith('ADC'):
+                label = getattr(Psu, sig)
+                self.current[label] = np.array([x, i_psu_adu_to_amp(x, label)])
+
+    def write_to_files(self, path):
+
+        for k in self.temperature:
+            np.savetxt(os.path.join(path, k + '.dat'), self.temperature[k].T, header=k, fmt=('%5d', '%6.1f'))
+
+        for k in self.voltage:
+            np.savetxt(os.path.join(path, k + '.dat'), self.voltage[k].T, header=k, fmt=('%5d', '%6.3f'))
+
+        for k in self.current:
+            np.savetxt(os.path.join(path, k + '.dat'), self.current[k].T, header=k, fmt=('%5d', '%6.3f'))
+
+        print("Calibration tables written to {}".format(path))
+
+    def plot(self, signal, xmin=BOUND_L, xmax=BOUND_U):
+
+        sig = signal[3:]
+
+        if sig in vars(Dpu):
+            xy = self.voltage[signal]
+            ylabel = 'Voltage [V]'
+        elif sig in vars(Temp):
+            xy = self.temperature[signal]
+            ylabel = 'Temperature [°C]'
+        elif sig in vars(Psu):
+            xy = self.current[signal]
+            ylabel = 'Current [A]'
+        else:
+            raise ValueError("Unknown signal '{}'".format(sig))
+
+        xref = np.linspace(xmin, xmax, 1000)
+        yref = calibrate(xref, signal)
+
+        limits = np.array((np.array(getattr(Limits, sig)), calibrate(np.array(getattr(Limits, sig)), signal))).T
+        print(limits)
+        fl, wl, wu, fu = limits
+        plt.axvspan(xmin, fl[0], alpha=0.25, color='red')
+        plt.axvspan(fl[0], wl[0], alpha=0.5, color='orange')
+        plt.axvspan(wu[0], fu[0], alpha=0.5, color='orange')
+        plt.axvspan(fu[0], xmax, alpha=0.25, color='red')
+
+        for i in limits:
+            plt.axhline(i[1], ls=':', color='grey')
+
+        plt.plot(xref, yref, color='grey', lw=0.5)
+        plt.plot(*xy, 'k.', label=signal, ms=4)
+        # plt.legend()
+        plt.xlabel('ADU')
+        plt.ylabel(ylabel)
+        plt.title(signal)
+        plt.grid(True)
+        plt.show()
+
+
+class Limits:
+    # raw operational limits (FAIL_L, WARN_L, WARN_U, FAIL_U)
+    ADC_P3V9 = (0x1D8D, 0x1E67, 0x2119, 0x21F3)
+    ADC_P3V3 = (0x27A2, 0x2912, 0x2DF2, 0x2F62)
+    ADC_P3V3_LVDS = (0x27A2, 0x2912, 0x2DF2, 0x2F62)
+    ADC_P2V5 = (0x215D, 0x2274, 0x26A1, 0x27B8)
+    ADC_P1V8 = (0x1BE0, 0x1CA9, 0x203A, 0x2103)
+    ADC_P1V2 = (0x172C, 0x17B2, 0x1ABE, 0x1B43)
+    ADC_REF = (0x215D, 0x2274, 0x26A1, 0x27B8)
+    ADC_TEMP1 = (0x210F, 0x2259, 0x2B37, 0x2C12)
+    ADC_TEMP_FEE = (0x17EA, 0x188E, 0x1CFD, 0x1D6B)
+    ADC_TEMP_CCD = (0x1968, 0x19DD, 0x1D20, 0x1D93)
+    ADC_I_FEE_ANA = (0xDC4, 0xDC4, 0x1D70, 0x1ECE)
+    ADC_I_FEE_DIG = (0xDC4, 0xDC4, 0x20FB, 0x22B4)
+    ADC_I_DPU = (0xDC4, 0xDC4, 0x20B7, 0x2269)
+    ADC_I_RSE = (0xDC4, 0xDC4, 0x1EA8, 0x2025)
+    ADC_I_HEATER = (0x152E, 0x152E, 0x23B2, 0x24F2)
+    ADC_PSU_TEMP = (0x12A5, 0x13E4, 0x298C, 0x2B08)
+
+    # raw upper RSE limits
+    RSE_MOTOR_TEMP = 0x96
+    RSE_ELEC_TEMP = 0x96
+
+    # raw ambient CCD limits
+    ADC_TEMP_CCD_AMB = (0x1968, 0x19DD, 0x29DB, 0x2A49)
+
+
+class LimitTables:
+
+    def __init__(self):
+
+        # temperatures
+        self.temperature = {}
+        for sig in vars(Temp):
+            if sig.startswith('ADC'):
+                label = getattr(Temp, sig)
+                adu_limits = np.array(getattr(Limits, sig))
+                self.temperature[label] = np.array([adu_limits, t_adu_to_deg(adu_limits, label)])
+
+        for sig in vars(Rse):
+            if sig.startswith('RSE'):
+                label = getattr(Rse, sig)
+                adu_limits = np.array(getattr(Limits, sig))
+                self.temperature[label] = np.array([adu_limits, t_adu_to_deg(adu_limits, label)])
+
+        # voltages
+        self.voltage = {}
+        for sig in vars(Dpu):
+            if sig.startswith('ADC'):
+                label = getattr(Dpu, sig)
+                adu_limits = np.array(getattr(Limits, sig))
+                self.voltage[label] = np.array([adu_limits, u_dpu_adu_to_volt(adu_limits, label)])
+
+        # currents
+        self.current = {}
+        for sig in vars(Psu):
+            if sig.startswith('ADC'):
+                label = getattr(Psu, sig)
+                adu_limits = np.array(getattr(Limits, sig))
+                self.current[label] = np.array([adu_limits, i_psu_adu_to_amp(adu_limits, label)])
+
+
+if __name__ == '__main__':
+    ct = CalibrationTables()
+    # ct.plot(Temp.ADC_PSU_TEMP)
+    lmt = LimitTables()
-- 
GitLab