Engee documentation
Notebook

Approaches to the design of neural regulators

Webinar Разработка перспективных видов регуляторов для объектов управления в Engee it consisted of several examples:

  1. Smartphone camera autofocus control

  2. Using a fuzzy controller to control давлением

  3. Implementation of adaptive and neural network regulators

This project contains the accompanying materials for the third part of the webinar, and you can study the rest in the community using the links above.

An example with a regular PID controller

This is the main model in which we will replace the regulator.

image.png

The liquid_pressure_regulator.engee model

Adaptive PID controller

First of all, we will set up an adaptive PID controller, the coefficients of which go through the following update procedure with each time step.:

    if abs(e) > c.thr
        c.Kp = max(0, c.Kp + clamp(c.α*e*c.pe, -c.ΔK, c.ΔK))
        c.Ki = max(0, c.Ki + clamp(c.α*e*c.pie, -c.ΔK, c.ΔK))
        c.Kd = max(0, c.Kd + clamp(c.α*e*c.pde, -c.ΔK, c.ΔK))
    end

First, we will not react to jumps in the control signal larger than a certain size. c.thr (parameter threshold). Secondly, the parameter α limits the rate of change of all parameters of the controller, while the step of change is modulo limited by the parameter ΔK.

image.png

The liquid_pressure_regulator_adaptive_pid.engee model

Neural regulator on Julia (RNN)

This controller contains the code of a recurrent neural network running on Julia without high-level libraries. His training takes place "online", during the operation of the system, so the quality of his work depends very much on the initial values of his weights. It may be necessary to apply a pre-training procedure, at least so that the neural network begins to produce a stable value when the pipeline state is stationary at the beginning of the calculation.

image.png

The liquid_pressure_regulator_neural_test.engee model

Creating a neural controller in C (FNN)

Let's launch the model and assemble the dataset

In [ ]:
Pkg.add("CSV")
In [ ]:
using DataFrames, CSV
In [ ]:
engee.open("$(@__DIR__)/liquid_pressure_regulator.engee")
data = engee.run( "liquid_pressure_regulator" )
Out[0]:
SimulationResult(
    "pressure" => WorkspaceArray{Float64}("liquid_pressure_regulator/pressure")
,
    "error" => WorkspaceArray{Float64}("liquid_pressure_regulator/error")
,
    "set_point" => WorkspaceArray{Float64}("liquid_pressure_regulator/set_point")
,
    "control" => WorkspaceArray{Float64}("liquid_pressure_regulator/control")

)
In [ ]:
function simout_to_df( data )
    vec_names = [i for i in keys(data) if length(data[i].value) > 0];
    df = DataFrame( hcat([collect(data[v]).value for v in vec_names]...), vec_names );
    df.time = collect(data[vec_names[1]]).time;
    return df
end
Out[0]:
simout_to_df (generic function with 1 method)
In [ ]:
df = simout_to_df( data );
CSV.write("Mode 1.csv", df);
plot(
    plot( df.time, df.set_point ), plot( df.time, df.control ), plot( df.time, df.pressure ), layout=(3,1)
)
Out[0]:

Let's change the parameters of the model and run it in a different scenario.

In [ ]:
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Amplitude"=>"0.001" )
data = engee.run( "liquid_pressure_regulator" )
df = simout_to_df( data );
CSV.write("Mode 2.csv", df);
plot(
    plot( df.time, df.set_point ), plot( df.time, df.control ), plot( df.time, df.pressure ), layout=(3,1)
)
Out[0]:
In [ ]:
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Amplitude"=>"0.0005", "Frequency"=>"0.2" )
data = engee.run( "liquid_pressure_regulator" )
df = simout_to_df( data );
CSV.write("3.csv mode", df);
plot(
    plot( df.time, df.set_point ), plot( df.time, df.control ), plot( df.time, df.pressure ), layout=(3,1)
)
Out[0]:

Putting all the model parameters back in place

In [ ]:
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Amplitude"=>"0.0005" )
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Frequency"=>"0.1" )

Let's train a neural network to approximate several regulators

This code allows you to prepare data, sets the structure of a three-layer fully connected neural network and trains it to find the relationship between the past 20 mismatch values and the next value of the control signal.

In [ ]:
Pkg.add(["Flux", "BSON", "Glob", "MLUtils"])
In [ ]:
using Flux, MLUtils
using CSV, DataFrames
using Statistics, Random
using BSON, Glob

# Initialize the random number generator for the sake of reproducibility of the experiment
Random.seed!(42)

# 1. Prepare the data
function load_and_preprocess_data_fnn()
    # Download all CSV files from the current folder
    files = glob("*.csv")
    dfs = [CSV.read(file, DataFrame, types=Float32) for file in files]
    
    # Combine data into one table
    combined_df = vcat(dfs...)
    
    # Let's extract the columns we need (time, error, control)
    vtime = combined_df.time
    error = combined_df.error
    control = combined_df.control
    
    # Data normalization (will greatly help with speeding up neural network learning)
    error_mean, error_std = mean(error), std(error)
    control_mean, control_std = mean(control), std(control)
    
    error_norm = (error .- error_mean) ./ error_std
    control_norm = (control .- control_mean) ./ control_std
    
    # We divide it into small sequences to train the RNN
    sequence_length = 20  # how many past steps do we take into account to predict the control signal
    X = []
    Y = []
    
    for i in 1:(length(vtime)-sequence_length)
        push!(X, error_norm[i:i+sequence_length-1])
        push!(Y, control_norm[i+sequence_length])
    end
    
    # Formalize them as arrays
    # X = reshape(hcat(X...), sequence_length, 1, :) # With butches
    X = hcat(X...)'
    Y = hcat(Y...)'
    
    return (X, Y), (error_mean, error_std, control_mean, control_std)
end


# 2. We define the structure of the model
function create_fnn_controller(input_size=20, hidden_size=5, output_size=1)
    return Chain(
        Dense(input_size, hidden_size, relu),
        Dense(hidden_size, hidden_size, relu),
        Dense(hidden_size, output_size)
    )
end


# 3. Learning with the new Flux API
function train_fnn_model(X, Y; epochs=100, batch_size=32)
    # Data separation
    split_idx = floor(Int, 0.8 * size(X, 1))
    X_train, Y_train = X[1:split_idx, :], Y[1:split_idx, :]
    X_val, Y_val = X[split_idx+1:end, :], Y[split_idx+1:end, :]
    
    # Creating a model and optimizer
    model = create_fnn_controller()
    optimizer = Flux.setup(Adam(0.001), model)
    
    # Loss function
    loss(x, y) = Flux.mse(model(x), y)
    
    # Preparing the DataLoader
    train_loader = Flux.DataLoader((X_train', Y_train'), batchsize=batch_size, shuffle=true)
    
    # The learning cycle
    train_losses = []
    val_losses = []
    
    for epoch in 1:epochs
        # Training
        Flux.train!(model, train_loader, optimizer) do m, x, y
            y_pred = m(x)
            Flux.mse(y_pred, y)
        end
        
        # Error calculation
        train_loss = loss(X_train', Y_train')
        val_loss = loss(X_val', Y_val')
        push!(train_losses, train_loss)
        push!(val_losses, val_loss)
        
        # Logging
        if epochs % 10 == 0
            @info "Epoch $epoch" train_loss val_loss
        end
    end
    
    # Visualization of learning
    plot(1:epochs, train_losses, label="Training Loss")
    plot!(1:epochs, val_losses, label="Validation Loss")
    xlabel!("Epoch")
    ylabel!("Loss")
    title!("Training Progress")
    
    return model
end

# 4. Evaluation of the model (unchanged)
function evaluate_fnn_model(model, X, Y, norm_params)
    predictions = model(X')
    
    # Denormalization
    _, _, control_mean, control_std = norm_params
    Y_true = Y .* control_std .+ control_mean
    Y_pred = predictions' .* control_std .+ control_mean
    
    # Calculation of metrics
    rmse = sqrt(mean((Y_true - Y_pred).^2))
    println("RMSE: ", rmse)
    
    # Visualization
    plot(Y_true[1:100], label="True Control Signal")
    plot!(Y_pred[1:100], label="Predicted Control Signal")
    xlabel!("Time Step")
    ylabel!("Control Signal")
    title!("FNN Controller Performance")
end
In [ ]:
# Uploading data
(X, Y), norm_params = load_and_preprocess_data_fnn()

# Training
model = train_fnn_model(X, Y, epochs=100, batch_size=32)

# Saving the model
using BSON
BSON.@save "fnn_controller_v2.bson" model norm_params
┌ Info: Epoch 1
│   train_loss = 0.8938878f0
└   val_loss = 0.065423325f0
┌ Info: Epoch 2
│   train_loss = 0.6930624f0
└   val_loss = 0.08745527f0
┌ Info: Epoch 3
│   train_loss = 0.6121955f0
└   val_loss = 0.0670045f0
┌ Info: Epoch 4
│   train_loss = 0.5751492f0
└   val_loss = 0.0673555f0
┌ Info: Epoch 5
│   train_loss = 0.5150536f0
└   val_loss = 0.084629685f0
┌ Info: Epoch 6
│   train_loss = 0.43911234f0
└   val_loss = 0.060537163f0
┌ Info: Epoch 7
│   train_loss = 0.34446087f0
└   val_loss = 0.06451357f0
┌ Info: Epoch 8
│   train_loss = 0.26789767f0
└   val_loss = 0.08551992f0
┌ Info: Epoch 9
│   train_loss = 0.21148369f0
└   val_loss = 0.10232961f0
┌ Info: Epoch 10
│   train_loss = 0.18503676f0
└   val_loss = 0.14733629f0
┌ Info: Epoch 11
│   train_loss = 0.15013915f0
└   val_loss = 0.14483832f0
┌ Info: Epoch 12
│   train_loss = 0.12138271f0
└   val_loss = 0.11467917f0
┌ Info: Epoch 13
│   train_loss = 0.10577717f0
└   val_loss = 0.045897737f0
┌ Info: Epoch 14
│   train_loss = 0.101480916f0
└   val_loss = 0.057284135f0
┌ Info: Epoch 15
│   train_loss = 0.10315049f0
└   val_loss = 0.065064624f0
┌ Info: Epoch 16
│   train_loss = 0.10541139f0
└   val_loss = 0.056540746f0
┌ Info: Epoch 17
│   train_loss = 0.09681009f0
└   val_loss = 0.045904756f0
┌ Info: Epoch 18
│   train_loss = 0.093385085f0
└   val_loss = 0.0685159f0
┌ Info: Epoch 19
│   train_loss = 0.09045938f0
└   val_loss = 0.041075245f0
┌ Info: Epoch 20
│   train_loss = 0.08972832f0
└   val_loss = 0.055446453f0
┌ Info: Epoch 21
│   train_loss = 0.09329716f0
└   val_loss = 0.03967251f0
┌ Info: Epoch 22
│   train_loss = 0.1414877f0
└   val_loss = 0.07507982f0
┌ Info: Epoch 23
│   train_loss = 0.08920607f0
└   val_loss = 0.051499482f0
┌ Info: Epoch 24
│   train_loss = 0.0801875f0
└   val_loss = 0.03534109f0
┌ Info: Epoch 25
│   train_loss = 0.08018934f0
└   val_loss = 0.030156119f0
┌ Info: Epoch 26
│   train_loss = 0.076051794f0
└   val_loss = 0.03466235f0
┌ Info: Epoch 27
│   train_loss = 0.07765352f0
└   val_loss = 0.056599125f0
┌ Info: Epoch 28
│   train_loss = 0.07806299f0
└   val_loss = 0.055766143f0
┌ Info: Epoch 29
│   train_loss = 0.07484163f0
└   val_loss = 0.0389787f0
┌ Info: Epoch 30
│   train_loss = 0.07139521f0
└   val_loss = 0.033857666f0
┌ Info: Epoch 31
│   train_loss = 0.08287221f0
└   val_loss = 0.042753786f0
┌ Info: Epoch 32
│   train_loss = 0.072352625f0
└   val_loss = 0.054598782f0
┌ Info: Epoch 33
│   train_loss = 0.0717976f0
└   val_loss = 0.031754486f0
┌ Info: Epoch 34
│   train_loss = 0.069250494f0
└   val_loss = 0.043800574f0
┌ Info: Epoch 35
│   train_loss = 0.068584874f0
└   val_loss = 0.035793144f0
┌ Info: Epoch 36
│   train_loss = 0.098963626f0
└   val_loss = 0.085103f0
┌ Info: Epoch 37
│   train_loss = 0.067444935f0
└   val_loss = 0.06019559f0
┌ Info: Epoch 38
│   train_loss = 0.080243886f0
└   val_loss = 0.027890693f0
┌ Info: Epoch 39
│   train_loss = 0.0734144f0
└   val_loss = 0.038119968f0
┌ Info: Epoch 40
│   train_loss = 0.06261252f0
└   val_loss = 0.036735766f0
┌ Info: Epoch 41
│   train_loss = 0.06626095f0
└   val_loss = 0.04766762f0
┌ Info: Epoch 42
│   train_loss = 0.061304063f0
└   val_loss = 0.038307376f0
┌ Info: Epoch 43
│   train_loss = 0.06375473f0
└   val_loss = 0.049757667f0
┌ Info: Epoch 44
│   train_loss = 0.06929615f0
└   val_loss = 0.031478032f0
┌ Info: Epoch 45
│   train_loss = 0.06482848f0
└   val_loss = 0.041360646f0
┌ Info: Epoch 46
│   train_loss = 0.08152386f0
└   val_loss = 0.031685207f0
┌ Info: Epoch 47
│   train_loss = 0.06899811f0
└   val_loss = 0.03166399f0
┌ Info: Epoch 48
│   train_loss = 0.06471846f0
└   val_loss = 0.057980362f0
┌ Info: Epoch 49
│   train_loss = 0.073723935f0
└   val_loss = 0.0419289f0
┌ Info: Epoch 50
│   train_loss = 0.062251564f0
└   val_loss = 0.05569823f0
┌ Info: Epoch 51
│   train_loss = 0.08304988f0
└   val_loss = 0.047913346f0
┌ Info: Epoch 52
│   train_loss = 0.13036466f0
└   val_loss = 0.06255697f0
┌ Info: Epoch 53
│   train_loss = 0.0668965f0
└   val_loss = 0.023209779f0
┌ Info: Epoch 54
│   train_loss = 0.0641162f0
└   val_loss = 0.026421405f0
┌ Info: Epoch 55
│   train_loss = 0.0606432f0
└   val_loss = 0.03451042f0
┌ Info: Epoch 56
│   train_loss = 0.05806036f0
└   val_loss = 0.047697715f0
┌ Info: Epoch 57
│   train_loss = 0.08080194f0
└   val_loss = 0.030021664f0
┌ Info: Epoch 58
│   train_loss = 0.075242944f0
└   val_loss = 0.027072791f0
┌ Info: Epoch 59
│   train_loss = 0.07018888f0
└   val_loss = 0.03701796f0
┌ Info: Epoch 60
│   train_loss = 0.20293671f0
└   val_loss = 0.039905872f0
┌ Info: Epoch 61
│   train_loss = 0.08879273f0
└   val_loss = 0.068481795f0
┌ Info: Epoch 62
│   train_loss = 0.06276142f0
└   val_loss = 0.044078235f0
┌ Info: Epoch 63
│   train_loss = 0.060167953f0
└   val_loss = 0.030940093f0
┌ Info: Epoch 64
│   train_loss = 0.05863377f0
└   val_loss = 0.033402573f0
┌ Info: Epoch 65
│   train_loss = 0.09663699f0
└   val_loss = 0.033131924f0
┌ Info: Epoch 66
│   train_loss = 0.053091682f0
└   val_loss = 0.026231134f0
┌ Info: Epoch 67
│   train_loss = 0.053288568f0
└   val_loss = 0.03478807f0
┌ Info: Epoch 68
│   train_loss = 0.062837504f0
└   val_loss = 0.032456074f0
┌ Info: Epoch 69
│   train_loss = 0.14382939f0
└   val_loss = 0.039194208f0
┌ Info: Epoch 70
│   train_loss = 0.057213098f0
└   val_loss = 0.03217173f0
┌ Info: Epoch 71
│   train_loss = 0.059472032f0
└   val_loss = 0.050639316f0
┌ Info: Epoch 72
│   train_loss = 0.061687153f0
└   val_loss = 0.041273717f0
┌ Info: Epoch 73
│   train_loss = 0.057540856f0
└   val_loss = 0.043549165f0
┌ Info: Epoch 74
│   train_loss = 0.06940068f0
└   val_loss = 0.030464195f0
┌ Info: Epoch 75
│   train_loss = 0.055994995f0
└   val_loss = 0.06194949f0
┌ Info: Epoch 76
│   train_loss = 0.06127405f0
└   val_loss = 0.035824254f0
┌ Info: Epoch 77
│   train_loss = 0.08695382f0
└   val_loss = 0.03958839f0
┌ Info: Epoch 78
│   train_loss = 0.068831f0
└   val_loss = 0.044648975f0
┌ Info: Epoch 79
│   train_loss = 0.06480649f0
└   val_loss = 0.049541153f0
┌ Info: Epoch 80
│   train_loss = 0.054308545f0
└   val_loss = 0.03448179f0
┌ Info: Epoch 81
│   train_loss = 0.062011868f0
└   val_loss = 0.033419278f0
┌ Info: Epoch 82
│   train_loss = 0.058451455f0
└   val_loss = 0.041000187f0
┌ Info: Epoch 83
│   train_loss = 0.056441877f0
└   val_loss = 0.040258046f0
┌ Info: Epoch 84
│   train_loss = 0.065997876f0
└   val_loss = 0.02149929f0
┌ Info: Epoch 85
│   train_loss = 0.14329454f0
└   val_loss = 0.024663093f0
┌ Info: Epoch 86
│   train_loss = 0.07271247f0
└   val_loss = 0.057003014f0
┌ Info: Epoch 87
│   train_loss = 0.05535151f0
└   val_loss = 0.050590448f0
┌ Info: Epoch 88
│   train_loss = 0.10478283f0
└   val_loss = 0.041117538f0
┌ Info: Epoch 89
│   train_loss = 0.05750017f0
└   val_loss = 0.027143845f0
┌ Info: Epoch 90
│   train_loss = 0.05710252f0
└   val_loss = 0.029968357f0
┌ Info: Epoch 91
│   train_loss = 0.058521483f0
└   val_loss = 0.04156654f0
┌ Info: Epoch 92
│   train_loss = 0.056501415f0
└   val_loss = 0.031160006f0
┌ Info: Epoch 93
│   train_loss = 0.06948501f0
└   val_loss = 0.036111105f0
┌ Info: Epoch 94
│   train_loss = 0.056522947f0
└   val_loss = 0.042170122f0
┌ Info: Epoch 95
│   train_loss = 0.07270187f0
└   val_loss = 0.045176893f0
┌ Info: Epoch 96
│   train_loss = 0.09828934f0
└   val_loss = 0.060680926f0
┌ Info: Epoch 97
│   train_loss = 0.059696835f0
└   val_loss = 0.03424492f0
┌ Info: Epoch 98
│   train_loss = 0.07078414f0
└   val_loss = 0.032400988f0
┌ Info: Epoch 99
│   train_loss = 0.05980099f0
└   val_loss = 0.041180395f0
┌ Info: Epoch 100
│   train_loss = 0.05841915f0
└   val_loss = 0.038568825f0
In [ ]:
# Evaluation
evaluate_fnn_model(model, X, Y, norm_params)
RMSE: 0.00137486
Out[0]:

Let's generate the C code for a fully connected neural network.

Let's apply the library Symbolics to generate the code, we will make several improvements so that it can be called from the block. C Function.

In [ ]:
Pkg.add("Symbolics")
In [ ]:
using Symbolics
@variables X[1:20]
c_model_code = build_function( model( collect(X) ), collect(X); target=Symbolics.CTarget(), fname="neural_net", lhsname=:y, rhsnames=[:x] )
Out[0]:
"#include <math.h>\nvoid neural_net(double* y, const double* x) {\n  y[0] = -0.17745794f0 + 0.50926423f0 * ifelse(-0.049992073f0 + -0.23742697f0 * ifelse(-1.8439064f0 + -0.54531264f0 * x[0] + 0.69540715f0 * x[9] + 0.13691874f0 * x[10] + 0.4638261f0 * x[11] + 0.68296975f0 *" ⋯ 48502 bytes ⋯ "8941267f0 * x[16] + -0.20798773f0 * x[17] + 0.046037998f0 * x[18] + 0.2163552f0 * x[1] + 0.17670809f0 * x[19] + 0.48885685f0 * x[2] + 0.33982033f0 * x[3] + 0.23923202f0 * x[4] + 0.44608107f0 * x[5] + -0.22155789f0 * x[6] + 0.18008044f0 * x[7] + 0.3575259f0 * x[8]));\n}\n"
In [ ]:
# Let's replace a few instructions in the code
c_fixed_model_code = replace( c_model_code,
                              "double" => "float",
                              "f0" => "f",
                              "  y[0]"=>"y" );

println( c_fixed_model_code[1:200] )
println("...")
# include <math.h>
void neural_net(float* y, const float* x) {
y = -0.17745794f + 0.50926423f * ifelse(-0.049992073f + -0.23742697f * ifelse(-1.8439064f + -0.54531264f * x[0] + 0.69540715f * x[9] + 0.1
...
In [ ]:
# Let's leave only the third line of this code.
c_fixed_model_code = split(c_fixed_model_code, "\n")[3]

c_code_standalone = """
float ifelse(bool cond, float a, float b) { return cond ? a : b; }

$c_fixed_model_code""";

println( c_code_standalone[1:200] )
println("...")
float ifelse(bool cond, float a, float b) { return cond ? a : b; }

y = -0.17745794f + 0.50926423f * ifelse(-0.049992073f + -0.23742697f * ifelse(-1.8439064f + -0.54531264f * x[0] + 0.69540715f * x[9]
...

Save the code to a file

In [ ]:
open("$(@__DIR__)/neural_net_fc.c", "w") do f
    println( f, "$c_code_standalone" )
end
image.png

The liquid_pressure_regulator_neural_fc.engee model

Conclusion

Obviously, we have been training neural networks for too short a time using too small an example. There are many steps that should make it possible to improve the quality of such a controller - for example, to apply more signals to the input of a neural network, or simply assemble a larger dataset for better offline learning. We have shown how to approach the development of a neural regulator and how to test it in the Engee model environment.