Engee documentation
Notebook

Simulation of pipeline pressure control using a fuzzy regulator

This example demonstrates the simulation of pressure control in a pipeline using a fuzzy regulator.

The model file contains two hydraulic circuits with control systems. The upper circuit is controlled by a PID controller, its description can be found in the Community.: Simulation of pressure control in трубопроводе and Pipeline pressure control. The setpoint signal in both circuits is represented by a block
Chart

The lower circuit has similar parameters of the physical blocks.

Model diagram:

liquid_pressure_regulator_fuzzy_1--1728316412037.png

The fuzzy controller is described using the block Engee Function. It allows you to use code in the Julia programming language. Learn more about working with Engee Function you can find out in the corresponding article документации.

To create a fuzzy controller, let's use the Julia programming language library called FuzzyLogic. To install it in Engee, you need to execute the following code cell:

In [ ]:
Pkg.add(["FuzzyLogic"])
In [ ]:
Pkg.add("FuzzyLogic")

We are launching the installed FuzzyLogic library and the Plots charting library.

In [ ]:
using FuzzyLogic
using Plots

In the code cell below, the Mamdani fuzzy inference system is being built using a macro @mamfis. The construction of such a system includes the definition of Membership functions. The function is declared tipper, which takes one argument delta_p. This variable will represent the input data that comes from the adder that calculates the difference between the set and measured pressure.

Accessory functions for pressure difference ranges and valve cross-sectional area outputs include:

  • Gaussian membership function GaussianMF(x, y) - where x is the average value determining the center of distribution, and y is the standard deviation
  • Triangular membership function TriangularMF(x, y, z) where x is the left corner of the triangle, z is the right, and y is the peak of the triangle
  • Trapezoidal accessory function TrapezoidalMF(x, y, z, k), where x is the lower-left corner of the trapezoid, k is the lower-right corner, y and z are the upper-left and upper-right corners, respectively

In the variable domain The ranges in which membership functions will be defined are defined.

Next, fuzzy logic rules are defined that relate input and output variables. For example:

If delta_p very high, then control_signal it will be completely open.
If delta_p high, then control_signal it will be partially open.
And so on for the rest of the conditions.

In [ ]:
fis = @mamfis function tipper(delta_p)::control_signal
    delta_p := begin
        domain = -50000.0:50000.0
        very_high = GaussianMF(17000.0, 3000.0)  # Очень высокое давление
        high = GaussianMF(15000.0, 4120.0)       # Высокое давление
        moderate = GaussianMF(-6000.0, 3000.0)       # Нормальное давление
        low = GaussianMF(-15000.0, 3000.0)       # Низкое давление
        very_low = GaussianMF(-30000.0, 3000.0)  # Очень низкое давление
    end

    control_signal := begin
        domain = 0.0:0.005
        fully_closed = TriangularMF(0.00000000001, 0.00000001, 0.000001)  # Полностью закрытая заслонка
        partially_closed = TriangularMF(0.0000009, 0.000005, 0.00001)  # Частично закрытая заслонка
        neutral = TriangularMF(0.00001, 0.00002, 0.0003)  # Нейтральное положение (половина открыта)
        partially_open = TrapezoidalMF(0.0003, 0.0009, 0.003, 0.0044)  # Частично открытая заслонка
        fully_open = TriangularMF(0.004, 0.0047, 0.005)  # Полностью открытая заслонка
    end

    # Работающие правила
    delta_p == very_high --> control_signal == fully_open
    delta_p == high --> control_signal == partially_open
    delta_p == moderate --> control_signal == neutral
    delta_p == low --> control_signal == partially_closed
    delta_p == very_low --> control_signal == fully_closed
end
Out[0]:
tipper

Inputs:
-------
delta_p ∈ [-50000.0, 50000.0] with membership functions:
    very_high = GaussianMF{Float64}(17000.0, 3000.0)
    high = GaussianMF{Float64}(15000.0, 4120.0)
    moderate = GaussianMF{Float64}(-6000.0, 3000.0)
    low = GaussianMF{Float64}(-15000.0, 3000.0)
    very_low = GaussianMF{Float64}(-30000.0, 3000.0)


Outputs:
--------
control_signal ∈ [0.0, 0.005] with membership functions:
    fully_closed = TriangularMF{Float64}(1.0e-11, 1.0e-8, 1.0e-6)
    partially_closed = TriangularMF{Float64}(9.0e-7, 5.0e-6, 1.0e-5)
    neutral = TriangularMF{Float64}(1.0e-5, 2.0e-5, 0.0003)
    partially_open = TrapezoidalMF{Float64}(0.0003, 0.0009, 0.003, 0.0044)
    fully_open = TriangularMF{Float64}(0.004, 0.0047, 0.005)


Inference rules:
----------------
delta_p is very_high --> control_signal is fully_open
delta_p is high --> control_signal is partially_open
delta_p is moderate --> control_signal is neutral
delta_p is low --> control_signal is partially_closed
delta_p is very_low --> control_signal is fully_closed


Settings:
---------
- MinAnd()
- MaxOr()
- MinImplication()
- MaxAggregator()
- CentroidDefuzzifier(100)

Plotting membership functions for input data:

In [ ]:
plot(fis, :delta_p)
Out[0]:

Plotting membership functions for the data output:

In [ ]:
plot(fis, :control_signal)
Out[0]:

In order to implement this fuzzy inference system as a control system, it is necessary to generate a function from it that does not depend on the library and is defined only by the basic constructs of the Julia language. To do this, you can use the function compilefis(fis), where the fis argument is a fuzzy inference system defined earlier and written to a variable. The result of code generation using this function is written to the asd variable.:

In [ ]:
asd = compilefis(fis)
Out[0]:
:(function tipper(delta_p)
      very_high = exp(-((delta_p - 17000.0) ^ 2) / 1.8e7)
      high = exp(-((delta_p - 15000.0) ^ 2) / 3.39488e7)
      moderate = exp(-((delta_p - -6000.0) ^ 2) / 1.8e7)
      low = exp(-((delta_p - -15000.0) ^ 2) / 1.8e7)
      very_low = exp(-((delta_p - -30000.0) ^ 2) / 1.8e7)
      ant1 = very_high
      ant2 = high
      ant3 = moderate
      ant4 = low
      ant5 = very_low
      control_signal_agg = collect(LinRange{Float64}(0.0, 0.005, 101))
      @inbounds for (i, x) = enumerate(control_signal_agg)
              fully_closed = max(min((x - 1.0e-11) / 9.99e-9, (1.0e-6 - x) / 9.9e-7), 0)
              partially_closed = max(min((x - 9.0e-7) / 4.1000000000000006e-6, (1.0e-5 - x) / 5.0e-6), 0)
              neutral = max(min((x - 1.0e-5) / 1.0e-5, (0.0003 - x) / 0.00028), 0)
              partially_open = max(min((x - 0.0003) / 0.0006000000000000001, 1, (0.0044 - x) / 0.0014000000000000002), 0)
              fully_open = max(min((x - 0.004) / 0.0007000000000000001, (0.005 - x) / 0.0002999999999999999), 0)
              control_signal_agg[i] = max(max(max(max(min(ant1, fully_open), min(ant2, partially_open)), min(ant3, neutral)), min(ant4, partially_closed)), min(ant5, fully_closed))
          end
      control_signal = ((2 * sum((mfi * xi for (mfi, xi) = zip(control_signal_agg, LinRange{Float64}(0.0, 0.005, 101)))) - first(control_signal_agg) * 0.0) - last(control_signal_agg) * 0.005) / ((2 * sum(control_signal_agg) - first(control_signal_agg)) - last(control_signal_agg))
      return control_signal
  end)

The result recorded in the asd variable is rewritten into a code cell.:

In [ ]:
function tipper(delta_p)
    very_high = exp(-((delta_p - 17000.0) ^ 2) / 1.8e7)
    high = exp(-((delta_p - 15000.0) ^ 2) / 3.39488e7)
    moderate = exp(-((delta_p - -6000.0) ^ 2) / 1.8e7)
    low = exp(-((delta_p - -15000.0) ^ 2) / 1.8e7)
    very_low = exp(-((delta_p - -30000.0) ^ 2) / 1.8e7)
    ant1 = very_high
    ant2 = high
    ant3 = moderate
    ant4 = low
    ant5 = very_low
    control_signal_agg = collect(LinRange{Float64}(0.0, 0.005, 101))
    @inbounds for (i, x) = enumerate(control_signal_agg)
            fully_closed = max(min((x - 1.0e-11) / 9.99e-9, (1.0e-6 - x) / 9.9e-7), 0)
            partially_closed = max(min((x - 9.0e-7) / 4.1000000000000006e-6, (1.0e-5 - x) / 5.0e-6), 0)
            neutral = max(min((x - 1.0e-5) / 1.0e-5, (0.0003 - x) / 0.00028), 0)
            partially_open = max(min((x - 0.0003) / 0.0006000000000000001, 1, (0.0044 - x) / 0.0014000000000000002), 0)
            fully_open = max(min((x - 0.004) / 0.0007000000000000001, (0.005 - x) / 0.0002999999999999999), 0)
            control_signal_agg[i] = max(max(max(max(min(ant1, fully_open), min(ant2, partially_open)), min(ant3, neutral)), min(ant4, partially_closed)), min(ant5, fully_closed))
        end
    control_signal = ((2 * sum((mfi * xi for (mfi, xi) = zip(control_signal_agg, LinRange{Float64}(0.0, 0.005, 101)))) - first(control_signal_agg) * 0.0) - last(control_signal_agg) * 0.005) / ((2 * sum(control_signal_agg) - first(control_signal_agg)) - last(control_signal_agg))
    return control_signal
end
Out[0]:
tipper (generic function with 2 methods)

By defining the function tipper() describing the fuzzy output system, we can plot the dependence of the output data on the input data. To do this, we determine the vector of pressure values from -50000 to 50,000 Pa, as previously written in the variable domain and we pass it through the function:

In [ ]:
x = collect(range(-50000.0,50000.0,10001));

Plotting the response curve of the resulting function:

In [ ]:
plot(x, tipper.(x), linewidth=3)
Out[0]:

There is a graph of the dependence of the valve opening area on the pressure difference between the setpoint and the signal from the sensor.

The function obtained by code generation can be written to a file. To do this, the asd variable in Expr format is converted to String format for further writing.:

In [ ]:
text_function = string(asd)
Out[0]:
"function tipper(delta_p)\n    very_high = exp(-((delta_p - 17000.0) ^ 2) / 1.8e7)\n    high = exp(-((delta_p - 15000.0) ^ 2) / 3.39488e7)\n    moderate = exp(-((delta_p - -6000.0) ^ 2) / 1.8e7)\n    low = exp(-((delta_p - -15000.0) ^ 2) / 1.8e7)\n    very_low = exp(-((delta_p" ⋯ 977 bytes ⋯ "xi for (mfi, xi) = zip(control_signal_agg, LinRange{Float64}(0.0, 0.005, 101)))) - first(control_signal_agg) * 0.0) - last(control_signal_agg) * 0.005) / ((2 * sum(control_signal_agg) - first(control_signal_agg)) - last(control_signal_agg))\n    return control_signal\nend"

Writing a function in string format to a file:

In [ ]:
f = open("/user/start/examples/controls/liquid_pressure_regulator_fuzzy/liquid_pressure_regulator_fuzzy.jl","w") # создание файла
write(f, text_function) # запись в файл
close(f) # закрытие файла

The resulting file, in .jl format, with the function tipper(), we put it in the block Engee Function and we perform using the function include(). To do this, go to the block parameters, click on the "Look under the mask" button, select the "Main" tab and click "Edit source code", enter in the window that opens:

struct Block <: AbstractCausalComponent; end

function (c::Block)(t::Real, delta_p)
    include("liquid_pressure_regulator_fuzzy.jl")
    return control_signal
end

Next, the model is launched and data is displayed on graphs.

Defining the function to load and run the model:

In [ ]:
function start_model_engee()
    try
        engee.close("liquid_pressure_regulator_fuzzy", force=true) # закрытие модели 
        catch err # в случае, если нет модели, которую нужно закрыть и engee.close() не выполняется, то будет выполнена её загрузка после catch
            m = engee.load("$(@__DIR__)/liquid_pressure_regulator_fuzzy.engee") # загрузка модели
        end;

    try
        engee.run(m) # запуск модели
        catch err # в случае, если модель не загружена и engee.run() не выполняется, то будут выполнены две нижние строки после catch
            m = engee.load("$(@__DIR__)/liquid_pressure_regulator_fuzzy.engee") # загрузка модели
            engee.run(m) # запуск модели
        end
end
Out[0]:
start_model_engee (generic function with 1 method)

Running the simulation

In [ ]:
try
    start_model_engee() # запуск симуляции с помощью специальной функции, реализованной выше
    catch err
    end;

Extracting data from the simout variable and writing it to variables:

In [ ]:
sleep(5)
result = simout;
res = collect(result)
Out[0]:
4-element Vector{WorkspaceArray}:
 WorkspaceArray("liquid_pressure_regulator_fuzzy/Датчик давления.1")
 WorkspaceArray("liquid_pressure_regulator_fuzzy/Нечёткий регулятор.1")
 WorkspaceArray("liquid_pressure_regulator_fuzzy/Датчик давления-1.1")
 WorkspaceArray("liquid_pressure_regulator_fuzzy/Chart.RefPress")

Recording of signals from the setpoint sensor and pressure sensors of both circuits into variables:

In [ ]:
PID_регулятор = collect(res[1])
Нечёткий_регулятор = collect(res[3])
Сигнал_задатчика = collect(res[4]);

Visualization of simulation results

In [ ]:
plot(PID_регулятор[:,1], PID_регулятор[:,2], linewidth=3, label="PID-регулятор")
plot!(Нечёткий_регулятор[:,1], Нечёткий_регулятор[:,2], linewidth=3, label="Нечёткий регулятор")
plot!(Сигнал_задатчика[:,1], Сигнал_задатчика[:,2], linewidth=3, label="Сигнал задатчика")
Out[0]:

Analyzing the graph, you can see that, unlike the PID controller, the fuzzy controller has significantly less overshoot, as well as a shorter duration of the transient process. But the fuzzy controller also has a disadvantage, its signal can often be shifted from the setpoint signal by a certain amount, but this can be corrected by more precise adjustment of the membership functions and decision rules.

Conclusion:

In this example, the simulation of two physical objects with different variants of automatic control systems, in particular with a PID controller and a fuzzy controller, was demonstrated. A fuzzy inference system was built, which was then transformed into a function suitable for embedding in the Engee Function block. The result is a fuzzy controller, which is not inferior in its characteristics to a PID controller.