# Calculating R Value from SpO₂

This guide demonstrates how to calculate the R value from a given SpO₂ level and use it to set up and play PPG waveforms with the AECG100 SDK.

Reference Code: `spo2_to_r_value_output_ppg.py`

## 1. Connect to the Device

Create an AECG100 instance and connect to your device. You can specify a COM port or let the SDK auto-select.

    device = AECG100('./AECG100x64-1.dll')
    if not device.connect(-1, 5000):  # -1: auto-select port, 5000ms timeout
        print('Error: device is not connected')
        sys.exit()

Or, if you know the COM port:

    device = AECG100('./SDK/windows/AECG100x64-1.dll')
    if not device.connect(9, 5000):  # COM9
        sys.exit("Error: Device connection failed.")

Check connection:

    print(f"Device connected (Main SN: {device.get_serial_number()}, PPG SN: {device.get_ppg_serial_number()})")

## 2. Calculate R Value from SpO₂

The R value is derived from the SpO₂ using a linear or quadratic equation:

- Linear: `SpO2 = A + B * R`
- Quadratic: `SpO2 = A + B * R + C * R^2`

Use the following function to calculate R from a target SpO₂:

    # Define valid range of R value
    MIN_R_Value = 0
    MAX_R_Value = 1.0

    def calculate_R_Value(target_spo2, degree, A, B, C = None):
        """
        Calculate R value from SpO2 using linear or quadratic equation.
        Returns the valid R value in [0, 1], or None if not found.
        """
        if degree == 1:
            # SpO2 = A + B * R_Value
            R_Value = (A - target_spo2) / B
            R_Values = [R_Value]

        elif degree == 2:
            # SpO2 = A + B * R_Value + C * (R_Value ^ 2)
            if C is None:
                print("Not enough argument to calculate R_Value")
                return None

            # Transforming into general quadratic equation form: a*R^2 + b*R + c = 0
            a = C
            b = B
            c = A - target_spo2

            # Computes the discriminant(D) of the quadratic equation to determine possible R-values
            # D = b^2 - 4*a*c
            D = b ** 2 - 4 * a * c

            if D < 0:
                # No real solution exists if the discriminant(D) is negative
                print("Error: Invalid argument and target SpO2 combination (no real root for R-value)")
                return None

            R_Value_1 = (-b + math.sqrt(D)) / (2 * a)
            R_Value_2 = (-b - math.sqrt(D)) / (2 * a)
            R_Values = [R_Value_1, R_Value_2]

        else:
            print("Invalid degree")
            return None

        valid_R_Value = select_valid_R(R_Values, degree)
        return valid_R_Value

    def select_valid_R(R_Values, degree):
        valid_R_Values = [R for R in R_Values if MIN_R_Value <= R <= MAX_R_Value]

        if len(valid_R_Values) == 1:
            R_Value = valid_R_Values[0]

        elif len(valid_R_Values) == 2:
            # the smaller R value is selected, as it typically corresponds to a higher SpO₂
            R_Value = min(valid_R_Values)
            print(
                f"Warning: Two valid R value found ({valid_R_Values[0]:.4f} & {valid_R_Values[1]:.4f})"
                f"Warning: The smaller value {R_Value:.4f} has been selected."
            )

        else:
            if degree == 1:
                print(
                    f"Calculated solutions ({R_Values[0]:.4f}) falls outside the reference range ({MIN_R_Value} ~ {MAX_R_Value})."
                )
            elif degree == 2:
                print(
                    f"Both calculated solutions ({R_Values[0]:.4f} and {R_Values[1]:.4f}) fall outside the reference range ({MIN_R_Value} ~ {MAX_R_Value})."
                )
            return None

        return R_Value

Example usage:

    target_spo2 = 85
    # R_Value = calculate_R_Value(target_spo2, 1, A=110, B=25)
    R_Value = calculate_R_Value(target_spo2, 2, A=111.8, B=-18, C=-11.7)

    if R_Value is None:
        device.free()
        sys.exit("No valid R value found for the given SpO2.")

## 3. Calculate PPG AC/DC Components

Once the R value is determined, the combinations of R_DC, R_AC, IR_DC, and IR_AC are constrained. Among these four parameters, any three can be given, and the remaining one can be derived using the following equations:

R_v a l u e=P I(R)/P I(I R)R\_value = PI(R) / PI(IR)R_v a l u e=P I(R)/P I(I R)

where the Perfusion Index (PI) is defined as:

P I=A C/D C PI = AC / DC P I=A C/D C

By combining the above two equations, a simplified expression is obtained:

R_v a l u e=R A C R D C∗I R D C I R A C R\_value = \frac{R_{AC}}{R_{DC}}*\frac{IR_{DC}}{IR_{AC}} R_v a l u e=R D C​R A C​​∗I R A C​I R D C​​

Example:

    R_DC = 400
    R_AC = None
    IR_DC = 400
    IR_AC = 20

    if R_DC is None:
        R_DC = (R_AC * IR_DC) / (R_Value * IR_AC)
    elif IR_AC is None:
        R_DC = (R_AC * IR_DC) / (R_Value * R_DC)
    elif R_AC is None:
        R_AC = (R_Value * R_DC * IR_AC) / IR_DC
    elif IR_DC is None:
        IR_DC = (R_Value * R_DC * IR_AC) / R_AC

    # Get default waveform parameters
    ppg_waveform_1 = device.get_default_ppg_ch1_waveform()          # R
    ppg_waveform_2 = device.get_default_ppg_ch2_waveform()          # IR
    ppg_waveform_1.VolDC = R_DC
    ppg_waveform_1.VolSP = R_AC
    ppg_waveform_2.VolDC = IR_DC
    ppg_waveform_2.VolSP = IR_AC

## 4. Rescale and Set Waveform Parameters

Set the frequency and time period for both channels, then rescale using SDK helpers:

    bpm = 60
    frequency = bpm / 60
    ppg_waveform_1.Frequency = ppg_waveform_2.Frequency = frequency
    ppg_waveform_1.TimePeriod = ppg_waveform_2.TimePeriod = int(1000 / frequency)

    ppg_waveform_1 = device.rescale_ppg_waveform(ppg_waveform_1)
    ppg_waveform_2 = device.rescale_ppg_waveform(ppg_waveform_2)

## 5. Play PPG Signals

Play two-channel PPG:

    device.play_ppg_ex(pointer(ppg_waveform_1), pointer(ppg_waveform_2), OutputSignalCallback(0), OutputSignalCallback(0))

Or single-channel:

    device.play_ppg(1, pointer(ppg_waveform_1), OutputSignalCallback(0))  # R channel
    device.play_ppg(2, pointer(ppg_waveform_2), OutputSignalCallback(0))  # IR channel