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:

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_value=PI(R)/PI(IR)R\_value = PI(R) / PI(IR)

where the Perfusion Index (PI) is defined as:

PI=AC/DC PI = AC / DC

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

R_value=RACRDCIRDCIRAC R\_value = \frac{R_{AC}}{R_{DC}}*\frac{IR_{DC}}{IR_{AC}}

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