Automated parameter sweep using Python

This application note shows how to use Python scripting to automate actions otherwise performed manually in Cockpit. Doing so, it also introduces a recommended workflow for working with the imperix Python library.

This example uses Python to automate a parameter sweep for the PI controller from TN109, used for the current control of a buck converter. The script iterates through a predefined table of (\(K_{p}\), \(K_{i}\)) pairs. For each pair, it updates the parameters, applies a current reference step, captures the transient response using the Scope module, and exports the resulting data.

While tuning a basic PI controller is a relatively straightforward task that may not require automation, this simple use case supports recommendations and guidelines on key concepts for automating tasks using the Python API.

Automated parameter sweep executed via Python script
Comparison of step responses using Cockpit snapshots

Prerequisites

Building the automation sequence in Python

This section divides the workflow into three parts and maps the standard manual actions in the Cockpit graphical interface to their Python API equivalents. Then, it combines these building blocks into a complete automated script.

Creating a project and launching the user code

To run the code, the model must first be built and deployed. After initiating the build (Ctrl + B in Simulink, Ctrl + Alt + B in PLECS), Cockpit opens automatically and generates a new project. Click the LINK TARGET button to view available targets. Once a target is selected, the code will execute automatically. For more detailed instructions, refer to PN138.

There are two options available to manage projects using Python:

Option 1: Connect to an existing project (recommended)

This approach connects to an active Cockpit project. First, launch the code manually by following the steps above. Then, in the Python script, retrieve the running project by referencing its name.

from imperix import Cockpit

with Cockpit() as cockpit:

    # ========================================================================
    # Retrieve an existing Cockpit project by name
    # ========================================================================
    project = cockpit.projects.get("TN109_PI_current_control")

    if project is None:
        raise SystemExit("Project not found.")
        
    # ...

Option 2: Re-create the project programmatically

Alternatively, a new Cockpit project can be created programmatically. While slightly more complex, this method ensures a clean workspace and prevents unintentional modifications to existing projects.

When using this approach, working with a standalone copy of the compiled user code (the .elf file) is recommended to avoid file conflicts. The code snippet below assumes this .elf file is located in the same directory as the Python script.

from imperix import Cockpit
from pathlib import Path

# The MAC address can be copy-pasted from Cockpit Target tab.
TARGET_MAC = "02:BB:62:75:88:2E"

# BASE_DIR is the directory containing this script.
BASE_DIR: Path = Path(__file__).resolve().parent
ELF_PATH: Path = BASE_DIR / "TN109_PI_current_control.elf"

with Cockpit() as cockpit:

    # ============================================================================
    # Verify that the targetted controller is available
    # ============================================================================

    cockpit.targets.refresh()

    target = cockpit.targets.get(TARGET_MAC)

    if target is None:
        raise SystemExit("Target not found.")
    
    # ========================================================================
    # Create a Cockpit project and launch the user code
    # ========================================================================

    project = cockpit.projects.create(
        ELF_PATH,
        name="PI controller sweep",
        overwrite=True,
    )

    project.link_target(TARGET_MAC)

    project.connect(auto_start_code=True, bypass_warnings=True)
    
    # ...

Configuring the Scope module transient generator

To capture the transient response to a reference current step, the Scope module is configured to acquire Iout_ref and Iout_meas and the transient generator is used to apply the current reference step to Iout_meas, as illustrated on the left.

The code snippet below shows the recommended approach for configuring a Scope module via the Python API: delete any existing Scope module in the project and create a new one. This ensures a completely clean configuration and guarantees reproducibility. Alternatively, the Scope module can be configured manually via the GUI and then retrieved using the Python script, as detailed in the Python API modules documentation.

# Applied current-reference step (in Amperes).
INITIAL_CURRENT_REF = 3.0
FINAL_CURRENT_REF = 5.0

# ...

with Cockpit() as cockpit:

    # ...
    
    # =========================================================================
    # Configure the scope module
    # =========================================================================
    
    # Iterate through modules and delete any existing Scope module
    for module in project.modules.list():
        if module.type == ModuleType.SCOPE:
            module.delete()

    # Create a new Scope module
    scope = project.modules.create(ModuleType.SCOPE)
    scope.add_variables(["Iout_ref", "Iout_meas"])
    scope.set_window(window_ms=15.0)
    scope.set_transient(
        variable="Iout_ref",
        positions_ms=[2.0],
        values=[FINAL_CURRENT_REF],
    )
    
    # ...

Automating the parameter sweep and acquisition

The following steps define the sequence for acquiring the current step response.

  1. Enable the PWM outputs.
  2. Initialize the current reference (Iout_ref) to 3 A.
  3. For each Kp and Ki par:
    1. Change Kp and Ki values.
    2. Trigger “Fire transient” to apply the configured reference step and acquire the resulting data.
    3. (Optional) Click on the camera icon to take a snapshot of acquired data. Curves from different snapshots can be compared directly within Cockpit to evaluate results across different Kp and Ki values (see PN300).
    4. Export the acquired plot data as a MAT file.
Acquisition of current step responses in Cockpit for multiple Kp and Ki pairs

The Python code required to automate these steps is shown below:

# ...

# The (Kp, Ki) pairs used in the sweep
GAIN_PAIRS = [
    (1.0, 100.0),
    (2.0, 200.0),
    (3.0, 300.0),
    # To be completed...
]

# BASE_DIR is the directory containing this script
BASE_DIR: Path = Path(__file__).resolve().parent
OUTPUT_DIR: Path = BASE_DIR / "output"
OUTPUT_DIR.mkdir(exist_ok=True)

# ...

with Cockpit() as cockpit:

    # ...
    
    # If an operation within 'try' fails, jumps to the 'finally' block. Used to
    # safely power down the converter before the script exits due to an error.
    try:

        # =====================================================================
        # Initialization
        # =====================================================================

        print("Enabling PWM outputs.\n")

        project.enable_pwm()
        project.variables["Iout_ref"] = INITIAL_CURRENT_REF
        project.clear_all_snapshots()

        # =====================================================================
        # Parameter sweep and data acquisition
        # =====================================================================
        for kp, ki in GAIN_PAIRS:
            print(f"Testing Kp={kp:g}, Ki={ki:g}")

            # Apply parameter values
            project.variables["Kp"] = kp
            project.variables["Ki"] = ki

            # Apply transient sequence and start acquisition
            scope.fire_transient()

            # Wait for the acquisition to complete
            cockpit.wait_until(scope.is_capture_done)

            # (Optional) Take a snapshot
            project.take_snapshot(
                name=f"kp_{kp:g}_ki_{ki:g}",
                comment=f"Kp = {kp:g}, Ki = {ki:g}")

            # Export acquired data in a MAT file
            scope.export(OUTPUT_DIR / f"kp_{kp:g}_ki_{ki:g}.mat")

    finally:

        # =====================================================================
        # Reset current reference to zero and disable PWM outputs
        # =====================================================================

        print(f"\nDisabling PWM outputs.\n")

        project.variables["Iout_ref"] = 0.0
        project.disable_pwm()

The complete script

The complete script is shown below:

from pathlib import Path

from imperix import Cockpit, ModuleType

# Cockpit project name to connect to
PROJECT_NAME = "TN109_PI_current_control"

# The (Kp, Ki) pairs used in the parameter sweep
GAIN_PAIRS = [
    (3.0,  8000.0),
    (3.0,  5000.0),
    (3.0,  3000.0),
    (3.0,  1500.0),
    (3.0,  500.0),
    (10.0, 500.0),
    (20.0, 500.0),
]

# Applied current-reference step (in Amperes)
INITIAL_CURRENT_REF = 3.0
FINAL_CURRENT_REF = 5.0

# BASE_DIR is the directory containing this script
BASE_DIR: Path = Path(__file__).resolve().parent
OUTPUT_DIR: Path = BASE_DIR / "output"
OUTPUT_DIR.mkdir(exist_ok=True)

with Cockpit() as cockpit:

    # =========================================================================
    # Retrieve an existing Cockpit project by name
    # =========================================================================

    project = cockpit.projects.get(PROJECT_NAME)

    if project is None:
        raise SystemExit("Project not found.")

    # =========================================================================
    # Configure the scope module
    # =========================================================================
    
    # Iterate through modules and delete any existing Scope module
    for module in project.modules.list():
        if module.type == ModuleType.SCOPE:
            module.delete()

    # Create a new Scope module
    scope = project.modules.create(ModuleType.SCOPE)
    scope.add_variables(["Iout_ref", "Iout_meas"])
    scope.set_window(window_ms=15.0)
    scope.set_transient(
        variable="Iout_ref",
        positions_ms=[2.0],
        values=[FINAL_CURRENT_REF],
    )

    # If an operation within 'try' fails, jumps to the 'finally' block. Used to
    # safely power down the converter before the script exits due to an error.
    try:

        # =====================================================================
        # Initialization
        # =====================================================================

        print("Enabling PWM outputs.\n")

        project.enable_pwm()
        project.variables["Iout_ref"] = INITIAL_CURRENT_REF
        project.clear_all_snapshots()

        # =====================================================================
        # Parameter sweep and data acquisition
        # =====================================================================
        for kp, ki in GAIN_PAIRS:
            print(f"Testing Kp={kp:g}, Ki={ki:g}")

            # Apply parameter values
            project.variables["Kp"] = kp
            project.variables["Ki"] = ki

            # Apply transient sequence and start acquisition
            scope.fire_transient()

            # Wait for the acquisition to complete
            cockpit.wait_until(scope.is_capture_done)

            # (Optional) Take a snapshot
            project.take_snapshot(
                name=f"kp_{kp:g}_ki_{ki:g}",
                comment=f"Kp = {kp:g}, Ki = {ki:g}")

            # Export acquired data in a MAT file
            scope.export(OUTPUT_DIR / f"kp_{kp:g}_ki_{ki:g}.mat")

    finally:

        # =====================================================================
        # Reset current reference to zero and disable PWM outputs
        # =====================================================================

        print(f"\nDisabling PWM outputs.\n")

        project.variables["Iout_ref"] = 0.0
        project.disable_pwm()

Experimental results

The video below demonstrates the execution of the automated parameter sweep script using the PI controller code from TN109 on the following buck converter setup:

  • One B-Box 4 controller with ACG SDK software
  • 1x PEB-800-40 half-bridge module (rated for 800V 40A)
  • 1x 2.2mH 32A inductor
  • 1x 8 Ω load resistor

To replicate this example, follow these steps:

  1. Build a buck converter following step-by-step instructions given in PN119.
  2. Download and run the code from TN109.
  3. Copy the automated parameter sweep script provided above and save it as a Python file (e.g., param_sweep.py).
  4. In the Python script, update the PROJECT_NAME variable with the exact project name found in the Cockpit project settings to ensure a perfect match.
  5. Execute the newly created .py script using either Cockpit’s built-in launcher or a standalone Python environment.
Automated parameter sweep and acquisition

Going further

The generated files can be inspected directly in MATLAB or processed further in Python. The snippet below demonstrates how to read the exported MAT files in Python using the h5py library and arrange the data into a DataFrame using the pandas library. For more details on working with MAT files generated by Cockpit, refer to PN141.

The data exported from the Scope module has the following internal structure:

kp_3_ki_500.mat
├── General_Header
│   └── ...                  (export metadata)
├── Variables_Header
│   └── ...                  (variable metadata)
├── Iout_ref                 (N rows, 2 columns)
└── Iout_meas                (M rows, 2 columns)

The following Python code provides an example of how to read this data:

from pathlib import Path

import h5py
import pandas as pd

# Replace this with the path to one MAT file exported by the Scope module.
OUTPUT_PATH = Path(r"C:\imperix\output\kp_3_ki_500.mat")

with h5py.File(OUTPUT_PATH, "r") as exported_file:
    # Access the measured-current data saved by the Scope module.
    iout_meas_data = exported_file["Iout_meas"]

    # Each exported variable is a 2 x N array in h5py.
    # [0, :] means "first row, all columns": the time values for all N samples.
    # [1, :] means "second row, all columns": the signal values for all N samples.
    time = iout_meas_data[0, :]
    iout_meas = iout_meas_data[1, :]

    # Build a labelled table with one row per acquired sample.
    exported_data = pd.DataFrame({
        "Time": time,
        "Iout_meas": iout_meas,
    })

# Display the first five rows of the table.
print(exported_data.head())

# ...