Table of Contents

Implement Simulation Routines

This guide shows you how to implement the routine execution functionality that allows your connector to run simulations based on instructions from CDF.

For detailed information about simulation routines in CDF, see the Simulation Routines documentation.

Prerequisites

You should have completed:

What you'll learn:

  • What routines and routine revisions are
  • The three-method pattern (SetInput, GetOutput, RunCommand)
  • Implementing RoutineImplementationBase
  • Mapping CDF instructions to simulator operations

Understanding Simulator Routines

A simulator routine is a folder that organizes different versions of simulator instructions. Each routine defines:

  1. Inputs - Values to set in the simulator before running
  2. Outputs - Values to read from the simulator after running
  3. Script - Step-by-step instructions for the simulation

Routine Revisions

Each routine revision is an immutable configuration that defines:

  • Inputs - Constant values (STRING, DOUBLE, arrays) or time series references
  • Outputs - Values to read and optionally save to time series
  • Script - Ordered list of stages and steps for execution

Routine Revision Structure

Routine Revision
├── Inputs (e.g., Temperature = 25°C, Pressure = 2 bar)
├── Outputs (e.g., FlowRate, Efficiency)
└── Script (ordered stages with steps)
    ├── Stage 1: Set inputs
    │   ├── Step 1: Set Temperature → Sheet1, Cell A1
    │   └── Step 2: Set Pressure → Sheet1, Cell A2
    ├── Stage 2: Run calculation
    │   └── Step 1: Command: Calculate
    └── Stage 3: Get outputs
        ├── Step 1: Get FlowRate ← Sheet1, Cell B1
        └── Step 2: Get Efficiency ← Sheet1, Cell B2

The SDK handles reading configuration, providing inputs, orchestrating execution, and storing outputs. You implement how to set inputs, read outputs, and run commands in the simulator.

The Three-Method Pattern

All routine implementations follow the same pattern, regardless of integration type:

public abstract class RoutineImplementationBase
{
    // Set an input value in the simulator
    public abstract void SetInput(
        SimulatorRoutineRevisionInput inputConfig,
        SimulatorValueItem input,
        Dictionary<string, string> arguments,
        CancellationToken token);

    // Get an output value from the simulator
    public abstract SimulatorValueItem GetOutput(
        SimulatorRoutineRevisionOutput outputConfig,
        Dictionary<string, string> arguments,
        CancellationToken token);

    // Run a command in the simulator
    public abstract void RunCommand(
        Dictionary<string, string> arguments,
        CancellationToken token);
}

This pattern is universal - whether you're using COM, TCP, REST, or any other integration method.

Step 1: Create the Routine Class

Create NewSimRoutine.cs:

using Microsoft.Extensions.Logging;
using CogniteSdk.Alpha;
using Cognite.Simulator.Utils;

public class NewSimRoutine : RoutineImplementationBase
{
    private readonly dynamic _workbook;

    private readonly ILogger _logger;

    private const int XlCalculationManual = -4135;

    public NewSimRoutine(dynamic workbook, SimulatorRoutineRevision routineRevision, Dictionary<string, SimulatorValueItem> inputData, ILogger logger) : base(routineRevision, inputData, logger)
    {
        _workbook = workbook;
        _logger = logger;
    }

    public override void SetInput(
        SimulatorRoutineRevisionInput inputConfig,
        SimulatorValueItem input,
        Dictionary<string, string> arguments,
        CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public override SimulatorValueItem GetOutput(
        SimulatorRoutineRevisionOutput outputConfig,
        Dictionary<string, string> arguments,
        CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public override void RunCommand(
        Dictionary<string, string> arguments,
        CancellationToken token)
    {
    }
}

This class inherits from RoutineImplementationBase, receives the workbook (Excel COM object) in the constructor, relies on the base class for script orchestration, and implements the three required methods.

Step 2: Implement SetInput

The SetInput method writes values to the simulator.

Understanding SetInput Parameters

public override void SetInput(
    SimulatorRoutineRevisionInput inputConfig,  // Configuration from CDF
    SimulatorValueItem input,                   // Value to set
    Dictionary<string, string> arguments,       // Where to set it
    CancellationToken token)                    // Cancellation support

Implementation

public override void SetInput(
    SimulatorRoutineRevisionInput inputConfig,
    SimulatorValueItem input,
    Dictionary<string, string> arguments,
    CancellationToken token)
{
    ArgumentNullException.ThrowIfNull(input);
    ArgumentNullException.ThrowIfNull(arguments);

    // Extract sheet and cell reference from arguments
    var sheetName = arguments["sheet"];
    var cellReference = arguments["cell"];

    // Get the worksheet by name
    dynamic worksheet = _workbook.Worksheets(sheetName);
    dynamic cell = worksheet.Range(cellReference);

    // Set value based on type
    if (input.ValueType == SimulatorValueType.DOUBLE)
    {
        var rawValue = (input.Value as SimulatorValue.Double)?.Value ?? 0;
        cell.Value = rawValue;

        _logger.LogDebug($"Set {sheetName}!{cellReference} = {rawValue}");
    }
    else if (input.ValueType == SimulatorValueType.STRING)
    {
        var rawValue = (input.Value as SimulatorValue.String)?.Value;
        cell.Formula = rawValue;

        _logger.LogDebug($"Set {sheetName}!{cellReference} = '{rawValue}'");
    }
    else
    {
        throw new NotImplementedException($"{input.ValueType} not supported");
    }

    // Store reference for later use
    var simulatorObjectRef = new Dictionary<string, string>
    {
        { "sheet", sheetName },
        { "cell", cellReference }
    };
    input.SimulatorObjectReference = simulatorObjectRef;
}

Key concepts: arguments from the script step specify WHERE to set the value, value extraction casts SimulatorValue to concrete types (Double, String), type handling uses different setter methods for different value types, and SimulatorObjectReference stores the variable identifier.

Step 3: Implement GetOutput

The GetOutput method reads values from the simulator.

Understanding GetOutput Parameters

public override SimulatorValueItem GetOutput(
    SimulatorRoutineRevisionOutput outputConfig,  // Configuration from CDF
    Dictionary<string, string> arguments,         // Where to read from
    CancellationToken token)                      // Cancellation support

Returns: SimulatorValueItem containing the value read from the simulator.

Implementation

public override SimulatorValueItem GetOutput(
    SimulatorRoutineRevisionOutput outputConfig,
    Dictionary<string, string> arguments,
    CancellationToken token)
{
    ArgumentNullException.ThrowIfNull(outputConfig);
    ArgumentNullException.ThrowIfNull(arguments);

    // Extract sheet and cell reference
    var sheetName = arguments["sheet"];
    var cellReference = arguments["cell"];

    // Get the worksheet by name
    dynamic worksheet = _workbook.Worksheets(sheetName);
    dynamic cell = worksheet.Range(cellReference);

    // Read value based on expected type
    SimulatorValue value;

    if (outputConfig.ValueType == SimulatorValueType.DOUBLE)
    {
        var rawValue = cell.Value;

        if (rawValue == null)
        {
            _logger.LogWarning($"Cell {sheetName}!{cellReference} is empty, using default");
            rawValue = 0.0;
        }
        var doubleValue = Convert.ToDouble(rawValue);
        value = new SimulatorValue.Double(doubleValue);
        _logger.LogDebug($"Read {sheetName}!{cellReference} = {doubleValue}");
    }
    else if (outputConfig.ValueType == SimulatorValueType.STRING)
    {
        var rawValue = (string)cell.Text;
        value = new SimulatorValue.String(rawValue);
        _logger.LogDebug($"Read {sheetName}!{cellReference} = '{rawValue}'");
    }
    else
    {
        throw new NotImplementedException($"{outputConfig.ValueType} not supported");
    }

    // Create reference for where we read from
    var simulatorObjectRef = new Dictionary<string, string>
    {
        { "sheet", sheetName },
        { "cell", cellReference }
    };

    // Return the output item
    return new SimulatorValueItem
    {
        ValueType = outputConfig.ValueType,
        Value = value,
        ReferenceId = outputConfig.ReferenceId,
        SimulatorObjectReference = simulatorObjectRef,
        TimeseriesExternalId = outputConfig.SaveTimeseriesExternalId,
    };
}

Key concepts: arguments specify WHERE to read, type conversion casts from COM dynamic to expected type, SimulatorValueItem wraps the value in SDK type, and metadata preservation includes reference ID and timeseries mapping.

Handling Null/Missing Values

Excel cells can be empty. Handle gracefully:

var rawValue = cell.Value;

if (rawValue == null)
{
    _logger.LogWarning($"Cell {sheetName}!{cellReference} is empty, using default");
    rawValue = 0.0;  // Or throw exception if required
}

var doubleValue = Convert.ToDouble(rawValue);
value = new SimulatorValue.Double(doubleValue);

Step 4: Implement RunCommand

The RunCommand method executes simulator-specific operations like triggering calculations, running solvers, saving/loading state, or any action that doesn't involve setting/getting values (for Excel, usually not needed as formulas calculate automatically).

Implementation

public override void RunCommand(
    Dictionary<string, string> arguments,
    CancellationToken token)
{
    ArgumentNullException.ThrowIfNull(arguments);
    var command = arguments["command"];

    switch (command)
    {
        case "Pause":
            {
                _workbook.Application.Calculation = XlCalculationManual;
                _logger.LogInformation("Calculation mode set to manual");
                break;
            }
        case "Calculate":
            {
                _workbook.Application.Calculate();
                _logger.LogInformation("Calculation completed");
                break;
            }
        default:
            {
                throw new NotImplementedException($"Unsupported command: '{command}'");
            }
    }
}

Step 5: Update NewSimClient.RunSimulation

Now wire the routine into the NewSimClient:

public async Task<Dictionary<string, SimulatorValueItem>?> RunSimulation(DefaultModelFilestate modelState, SimulatorRoutineRevision routineRev, Dictionary<string, SimulatorValueItem> inputData, CancellationToken token)
{
    ArgumentNullException.ThrowIfNull(modelState);
    await semaphore.WaitAsync(token).ConfigureAwait(false);
    dynamic? workbook = null;
    try
    {
        Initialize();
        workbook = OpenBook(modelState.FilePath);

        var routine = new NewSimRoutine(workbook, routineRev, inputData, logger);
        return routine.PerformSimulation(token);
    }
    finally
    {
        if (workbook != null)
        {
            workbook.Close(false);
        }
        Shutdown();
        semaphore.Release();
    }
}

PerformSimulation iterates through script steps in order, calling your SetInput, GetOutput, or RunCommand methods based on step type, handling cancellation, and returning a dictionary of output values.

Step 6: Test with CDF

Now create a routine in CDF and test it.

Create an Excel Test Model

Create a simple Excel file (test-model.xlsx):

A B
10 =A1 * 2

Save it and upload to CDF.

Create a Routine and Revision

CDF -> Simulators -> Routines -> Create Routine

Create Routine

Setting inputs:

Set the first input (Sheet: Sheet1, Cell: A1, Value : 25) Set the second input (Sheet: Sheet1, Cell: B1, Value: "=A1 * 2") Set the output (Sheet: Sheet1, Cell: B1)

Create Routine

Routine in CDF

Routine in CDF

Run the Routine

In the CDF UI, navigate to Simulators > Routines, find "Simple Calculation", click Run now, and view the results in the Run browser.

Routine in CDF

Expected result: Output "Result" = 50.0

Routine output

Understanding Arguments

Arguments connect the routine script to your implementation.

Defining Arguments in SimulatorDefinition

Remember in SimulatorDefinition.cs:

StepFields = new List<SimulatorStepField>
{
    new SimulatorStepField
    {
        StepType = "get/set",
        Fields = new List<SimulatorStepFieldParam>
        {
            new SimulatorStepFieldParam
            {
                Name = "sheet",
                Label = "Sheet Name",
                Info = "Name of the worksheet (e.g., 'Sheet1')",
            },
            new SimulatorStepFieldParam
            {
                Name = "cell",
                Label = "Cell Reference",
                Info = "Excel cell reference (e.g., 'A1', 'B2', 'C3')",
            },
        },
    },
}

This defines what arguments the CDF API will ask for when creating routine steps.

Using Arguments in Script

When a user creates a step in CDF, they provide values for these arguments:

{
  "stepType": "Set",
  "arguments": {
    "referenceId": "INPUT1",
    "sheet": "Sheet1",
    "cell": "A5"
  }
}

Accessing Arguments in Code

public override void SetInput(
    SimulatorRoutineRevisionInput inputConfig,
    SimulatorValueItem input,
    Dictionary<string, string> arguments,
    CancellationToken token)
{
    // Arguments dictionary contains script-provided values
    var sheetName = arguments["sheet"];    // "Sheet1"
    var cellReference = arguments["cell"]; // "A5"

    // Use them to locate where to set the value
    dynamic worksheet = _workbook.Worksheets(sheetName);
    dynamic cell = worksheet.Range(cellReference);
    cell.Value = (input.Value as SimulatorValue.Double)?.Value;
}

Important: Argument names in SimulatorDefinition must exactly match keys used in your code.


Next: Continue to Testing to learn how to test your connector.