Http Client

Tutorial

Create Project

On the menu bar, create a new project.

File -> New

A pop-up window will appear where project name needs to be provided. Provide a meaningful name for the project (e.g., name of the example name you are currently testing).

Add a Digital Twin

On the menu bar, add a digital twin.

Project -> Add Digital Twin

A pop-up window will appear where project name needs to be provided. Provide a meaningful name for the simulation environment (e.g., name of the example name you are currently testing).

Setup Simulation Parameters

For this example, we are not simulating time series, but directly the Wave function. Because of this, set

Add Constant/Simulated variables

The purpose of this example is to reproduce the examples goal, where function to simulate is,

\[\begin{gather*} Z=cos(X)*sin(Y) \end{gather*}\]

From function, we see that z is our variable to simulate depending on x and y values. Add 3 variables within the Constant/Simulated variables panel as follows,

Type

Name

Sim. Min.

Sim. Max.

Sim. Res.

Constant

x

-10

10

0.1

Constant

y

-10

10

0.1

Simulated

z

NaN

NaN

NaN

Note

z values are NaN as they are not provided via the panel but within the custom code. They are calculated at simulation runtime.

Reset/Update Custom Code

Because new variable have been added, custom code layout/template needs to be updated.

To do so click either button,

Because we don’t have any code written so far, we can directly click on Reset Code.

After performing this action, you will be able to see how the mandatory regions have been automatically updated by the digital twin,

Before

#region ExtendedVariables
// Not currently in use
#endregion
...
public override void Initialize()
{
    ...
    #region MandatoryCodeIni
    this.XAxis.SimMin = TOBESET; // Start value
    this.XAxis.SimRes = TOBESET; // Step (Resolution)
    this.XAxis.SimMax = TOBESET; // End value
    #endregion
}

public override void EvaluateStep()
{
    ...
    #region MandatoryCodeEval
    // Not currently in use
    #endregion
}

After

#region ExtendedVariables
// Constant variables
private ExtendedVariable x; // x
private ExtendedVariable y; // y
// Simulated variables
private ExtendedVariable z; // z
#endregion
...
public override void Initialize()
{
    ...
    #region MandatoryCodeIni
    this.XAxis.SimMin = 1.0d; // Start value
    this.XAxis.SimRes = 1.0d; // Step (Resolution)
    this.XAxis.SimMax = 1.0d; // End value
    #endregion
}

public override void EvaluateStep()
{
    ...
    #region MandatoryCodeEval
    // Simulated variables (ti) - Values.
    this.z.Value = TOBESET;
    #endregion
}

Python Code / API

For this example, we will implement our Wave function instead of directly within the digital twin custom code, in an external source.

To do so we will even use another programming language (Python).

Create Python code

Create a folder named ExternalCode.

Now create a file within it called functions.py with following content

# importing "math" for mathematical operations 
import math

def wave(x, y):
    return math.cos(x)*math.sin(y)

As you can see right now we have the same function we defined in the Out of the Box example within the CustomCodeEval region but now in an external python file.

Create a Python API

Once the python code is created, we need to create a Web API that serves that code, allowing clients to connect and perform requests (e.g., executing our wave(x, y) function).

To do so, create a file also within the ExternalCode folder named server.py with following content,

#!/usr/bin/env python
# encoding: utf-8

# API modules
import json
from flask import Flask, request, jsonify

# Code modules
import functions

# App - Instance
app = Flask(__name__)

# Endpoints
@app.route('/wave', methods=['GET'])
def wave():
    try:
        # Get input variables from request
        x = float(request.args.get('x'))
        y = float(request.args.get('y'))

        # Execute python script
        z = wave(x, y)

        response = {
        "x": x,
        "y": y,
        "z": z
        }

        return jsonify(response)

    except:
        print("Something else went wrong")

@app.route('/')
def index():
    return jsonify({'Error': 'Use a specific endpoint from this API :)'})

# App - Run (Use 'waitress' for production mode!)
if __name__ == "__main__":
    from waitress import serve
    serve(app, host="127.0.0.1", port=5003)

From previous server.py code, we see that we import the previously created module functions and we use its function wave(x,y).

We can now run our python server by opening a console/terminal within the ExternalCode directory and run following command,

python server.py

If there are no issues with previous code, API will start running and available for us to call it from within the digital twin with an Http Client.

Note

The purpose of this tutorial is not how to create/build a Python API, but how to consume it from within the toolbox inbuilt Http Client.

Create an HttpClient

Within the region UserCustomVariables declare an Http Client as follows,

#region UserCustomVariables   
private HttpClient httpClient = null;
#endregion

Error

Http Client must be declared on the the global scope (UserCustomVariables region) so that it can be used within all simulations (combinations).

Now, within the Initialize method - CustomCodeIni region we will add the following lines,

#region CustomCodeIni
// Avoid creating an HttpClient foreach simulation!
if(this.httpClient == null)
{		
    this.httpClient = new HttpClient();
}
#endregion

Use the Http Client

Once the Http Client is instantiated, we need to create the http request. To do so, we use the EvaluateStep method - CustomCodeEval region were we will,

  1. Create a dictionary to hold variables x and y and its values.

    Warning

    Use .ToString(CultureInfo.InvariantCulture). This will avoid possible conflict if Python API and Toolbox have different cultures (e.g., decimal separator can be either ‘.’ or ‘,’).

    Warning

    Dictionary is a key-value pair. Make sure that the key name matches both in Python API and here.

  2. Define the request endpoint. That is the Uri where server is listening to and the method.

    Warning

    Make sure the IP Address/Uri is the same one as in the server.py file (e.g., host="127.0.0.1", port=5003).

  3. Use QueryHelpers to build a request the request.

  4. Call the python server with a Get request.

    Warning

    Do not use asynchronous requests/methods. either await or use .Result

  5. Ensure response status is Successful.

  6. Read and deserialize response content (e.g., into another dictionary)

    Warning

    Do not use asynchronous requests/methods. either await or use .Result

  7. Get z result value from deserialized dictionary

    Warning

    Use overload with Culture set to CultureInfo.InvariantCulture when parsing.

After following previous steps we implemented all required code to build a request, call the api and get results back to consume the within the digital twin. Following code block reproduces previous points from a code perspective.

#region CustomCodeEval
// 1 - Request Dictionary (Inputs)
Dictionary<string, string> requestInputs = new Dictionary<string, string>();
requestInputs.Add("x", this.x.Value.ToString(CultureInfo.InvariantCulture));
requestInputs.Add("y", this.y.Value.ToString(CultureInfo.InvariantCulture));

// 2 - Python API and Endpoints
string apiEndpoint = "http://127.0.0.1:5003";
string apiWaveEndpoint = apiEndpoint + "/wave";

// 3 - Query Builder
string apiWaveGetRequest = QueryHelpers.AddQueryString(apiWaveEndpoint, requestInputs);

// 4 - Call the Wave endpoint (awaiting; synchronously)
HttpResponseMessage apiWaveGetResponse = this.httpClient.GetAsync(apiWaveGetRequest).Result;

// 5 - Ensure response is successful
apiWaveGetResponse.EnsureSuccessStatusCode();

// 6 - Read and deserialize response content
string apiWaveGetResponseContent = apiWaveGetResponse.Content.ReadAsStringAsync().Result;
Dictionary<string, string> requestOutputs = JsonConvert.DeserializeObject<Dictionary<string, string>>(apiWaveGetResponseContent);

// 7 - Get `z` result value
double zResult = double.Parse(requestOutputs["z"], CultureInfo.InvariantCulture);
#endregion

Provide Simulated variables values

If we click on Validate Code, we will get an error similar to,

Error (CS0103). Line: 66, Column: 17. The name 'TOBESET' does not exist in the current context

This indicates that we still need to provide a value four our simulated variable z to be able to validate (compile) code properly.

To pre-calculate the value of z depending on x and y, we can use the CustomCodeEval region and replace the TOBESET by calculated value as follows,

#region MandatoryCodeEval
// Simulated variables (ti) - Values.
this.z.Value = zResult;
#endregion

If we click on Validate Code again we will now get a successful code compilation,

Compile succeeded. Code updated.

Simulate

After having everything setup, we can proceed to simulate (clicking Sim / Opt button) the digital twin with all possible combinations (40401 for this example).

Once simulation finishes, a report can be generated (clicking Report button). This will add a report on the Reports tab ready to be explored.

Note

Current example is not a time series, so in the 2D view of the report, only points will appear on the plots. Switch to the 3D view mode to reproduce the waves plot.

Files

Project generated on the previous tutorial section can be downloaded,