Документация Engee
Notebook

Оценка недостающих параметров модели

Если думать о создании моделей объекта управления с точки зрения данных, которые у нас есть о модели, то часто разделяют 3 подхода:
Модель белого ящика – это модель, в которой мы знаем всю основную динамику системы.
Напротив, модель черного ящика подразумевает то, что мы не знаем динамику вообще.
В этом примере разбирается промежуточный подход серого ящика, когда мы знаем структуру модели, но нам недостает некоторых параметров.
Недостающие параметры можно подобрать по экспериментальным данным. Это довольно сложный алгоритм, но благодаря совместной работе скриптов и моделей, в коде можно реализовать даже такую непростую задачу.

Загрузка данных из таблицы
In [ ]:
using DataFrames, CSV;
In [ ]:
Import_data = "/user/start/examples/controls/parameter_estimation_webinar/parameter_estimation.csv" # @param {type:"filepicker"}
table = CSV.read(Import_data, DataFrame);
print(".csv файл импортирован")
Показать_таблицу = true # @param {type:"boolean"}
if Показать_таблицу
    display(table)
end  
.csv файл импортирован
1001×3 DataFrame
976 rows omitted
RowtimeСтупенчатая функцияAdd
Float64Float64Float64
10.00.00.197934
20.010.00.631221
30.020.0-0.205922
40.030.0-1.02133
50.040.00.48395
60.050.0-0.200081
70.060.0-0.234782
80.070.0-0.52721
90.080.0-0.632835
100.090.00.791541
110.10.0-0.18815
120.110.0-0.39816
130.120.0-0.066747
⋮⋮⋮⋮
9909.8912.050.9197
9919.912.053.021
9929.9112.051.6726
9939.9212.051.0411
9949.9312.051.6731
9959.9412.051.402
9969.9512.051.2705
9979.9612.052.398
9989.9712.051.0498
9999.9812.052.5656
10009.9912.052.7554
100110.012.051.5298
In [ ]:
Столбец_времени = 1 # @param {type:"integer"}
Столбец_входа = 2 # @param {type:"integer"} 
Столбец_выхода = 3 # @param {type:"integer"}
Начинать_со_строки = 1 # @param {type:"integer"} 
exp_time_data = table[Начинать_со_строки:end, Столбец_времени]; 
exp_in_data = table[Начинать_со_строки:end, Столбец_входа];
exp_out_data = table[Начинать_со_строки:end, Столбец_выхода];

В этом примере объект управления – двигатель постоянного тока. В модели указаны все известные параметры из документации на двигатель. Но трение в документации не дано, поэтому сейчас в блоках коэффициентов вязкого трения и трения скольжения находятся примерные значения.

Следующая ячейка запускает модель на симуляцию, чтобы убедиться, что результаты модели будут далеки от экспериментальных данных.

Сравнение экспериментальных данных и данных модели
In [ ]:
model_name = "parameter_estimation" # @param {type:"string"}
input_name = "Из рабочей области" # @param {type:"string"}

if model_name in [m.name for m in engee.get_all_models()]
    m = engee.open( model_name ) # загрузка модели
else
    m = engee.load("$(model_name).engee" )
end

df_in = DataFrame(time=exp_time_data, value=exp_in_data)
df_in = Float64.(df_in);
wa_in = WorkspaceArray(string(rand()), df_in)
engee.set_param!("$(model_name)/$input_name", "VariableName" =>"wa_in") 


logout = engee.run(m, verbose=false)
input = plot(exp_time_data, exp_in_data, title = "Вход", xlabel="t, c", label = "эксперимент", legend=true)
output = plot(exp_time_data, exp_out_data, title = "Выход", xlabel="t, c", label = "эксперимент", legend=true)
plot!(logout["w"].time, logout["w"].value, label = "моделирование", linewidth=2)
plot(input, output, layout = (2, 1))
Out[0]:
Подключение библиотек для оценки
In [ ]:
using DataFrames
try
    using NLopt
catch
    Pkg.add(["NLopt"])
    using NLopt
end
try
    using Interpolations
catch
    Pkg.add(["Interpolations"])
    using Interpolations
end
In [ ]:
# @markdown # Оценка параметров модели
# @markdown ## Для начала оценки параметров модели введите:
# @markdown ### Название модели:
# Открываем модель
model_name = "parameter_estimation" # @param {type:"string"}
if model_name in [m.name for m in engee.get_all_models()] # Проверка условия загрузки модели в ядро
    model = engee.open(model_name) # Открыть модель
    model = engee.gcm() 
else
    @error "Модель $model_name не открыта."
end
# @markdown ### Вход (блок типа FromWorkspace):
input_name = "Из рабочей области" # @param {type:"string"}
input_path = engee.find_system(model; depth=0, blockparams=["BlockType" => "FromWorkspace","BlockName"=>input_name])
# @markdown ### Значение входного сигнала:
input_data = [exp_time_data exp_in_data] # @param {type:"raw"}
typeof(input_data) === Matrix{Float64} || throw(ArgumentError("Входной сигнал должен быть матрицей содержащей числа типа Float64."))
size(input_data,2) == 2 || throw(ArgumentError("Входной сигнал должен иметь 2 столбца - время и значения сигнала."))

isempty(engee.find_system(model; depth=0, blockparams=["BlockType" => "FromWorkspace","BlockName"=>input_name])) && throw(ArgumentError("Блока $input_name не существует в корневой системе модели $(model.name)."))
pe_udata_workspace = WorkspaceArray("pe_udata_workspace", DataFrame(time = input_data[:,1], value = input_data[:,2]));
engee.set_param!("$(model.name)/$input_name", "VariableName" =>"pe_udata_workspace") 

# @markdown ### Выход (логируемый сигнал):
output_name = "w" # @param {type:"string"}
output_name in keys(engee.run(model, verbose=false)) || throw(ArgumentError("Сигнал $output_name не найден среди логируемых сигналов."))

# @markdown ### Значение выходного сигнала:
output_data = [exp_time_data exp_out_data] # @param {type:"raw"}
typeof(output_data) === Matrix{Float64} || throw(ArgumentError("Выходной сигнал должен быть матрицей, содержащей числа типа Float64."))
size(output_data,2) == 2 || throw(ArgumentError("Выходной сигнал должен иметь 2 колонки - время и значения сигнала."))

# @markdown ### Имена оцениваемых параметров:
# Создаем вектор переменных, их начальные значения и ограничения
p_names = ["b","c"] # @param {type:"raw"}
p_names isa Vector{String} || throw(ArgumentError("Названия параметров должны быть введены в виде вектора строк."))
parametrs_number = length(p_names)
parametrs_number>0 || throw(ArgumentError("Количество параметров должно быть больше нуля."))

# @markdown ### Начальные значения параметров:
p_0 = [0.01, 0.7] # @param {type:"raw"}
p_0 isa Vector{Float64} || throw(ArgumentError("Начальные значения парметров должны быть вектором, содержащим числа типа Float64."))
parametrs_number==length(p_0) || throw(ArgumentError("Количество начальных значений и парметров должно совпадать."))

# @markdown ### Нижняя граница значений параметров:
p_lower_bounds = [0.0, 0.0] # @param {type:"raw"}
p_lower_bounds isa Vector{Float64} || throw(ArgumentError("Нижняя граница значений параметров должна быть вектором, содержащим числа типа Float64."))
parametrs_number==length(p_lower_bounds) || throw(ArgumentError("Количество значений нижней границы и парметров должно совпадать."))

# @markdown ### Верхняя граница значений параметров:
p_upper_bounds = [1.0, 10.0] # @param {type:"raw"}
p_upper_bounds isa Vector{Float64} || throw(ArgumentError("Верхняя граница значений параметров должна быть вектором, содержащим числа типа Float64."))
parametrs_number==length(p_0) || throw(ArgumentError("Количество начальных значений и парметров должно совпадать."))

# @markdown ## Настройки оптимизации:
# @markdown ### Алгоритм оптимизации ("G" в начале названия алгоритма говорит о том что он глобальный, "L" - локальный)
algoritm = :G_MLSL # @param [:GN_DIRECT,:GN_DIRECT_L,:GN_DIRECT_L_RAND,:GN_CRS2_LM,:G_MLSL,:GN_AGS,:GN_ISRES,:GN_ESCH,:LN_COBYLA,:LN_BOBYQA,:LN_PRAXIS,:LN_NELDERMEAD,:LN_SBPLX] {type:"raw"}
# @markdown ### Вспомогательный алгоритм (только для :G_MLSL)
local_algoritm = :LN_COBYLA # @param [:LN_COBYLA,:LN_BOBYQA,:LN_PRAXIS,:LN_NELDERMEAD,:LN_SBPLX] {type:"raw"}
# Настройка параметров оптимизации
# @markdown ### Минимальное относительное изменение шага
xtol_rel = 1.0e-1 # @param {type:"number"}
xtol_rel>0 || throw(ArgumentError("Минимальное относительное изменение шага должно быть положительным."))
# @markdown ### Минимальное абсолютное изменение шага
xtol_abs = 1.0e-1 # @param {type:"number"}
xtol_rel>0 || throw(ArgumentError("Минимальное абсолютное изменение шага должно быть больше положительным."))
# @markdown ### Минимальное относительное изменение целевой функции
ftol_rel = 1.0e-1 # @param {type:"number"}
xtol_rel>0 || throw(ArgumentError("Минимальное относительное изменение целевой функции должно быть положительным."))
# @markdown ### Минимальное абсолютное изменение целевой функции
ftol_abs = 1.0e-1 # @param {type:"number"}
xtol_rel>0 || throw(ArgumentError("Минимальное абсолютное изменение целевой функции должно быть положительным."))
# @markdown ### Максимальное количество оценок целевой функции
maxeval = 30 # @param {type:"integer"}
xtol_rel>0 || throw(ArgumentError("Максимальное количество оценок целевой функции быть больше нуля."))

# @markdown ## Отображение результатов оценки параметров
# @markdown ### График измеренных данных и результатов моделирования:
compare = true # @param {type:"boolean"}
# @markdown ### График ошибок между измеренными данными и результатами моделирования:
reside = true # @param {type:"boolean"}
# @markdown ### График значений оценок параметров:
variables = true # @param {type:"boolean"}
# @markdown ### График значений целевой функции:
objective = true # @param {type:"boolean"}

#График сравнения выхода модели и измеренных данных
function compare_plot(model, output_name::String, tdata::Vector{Float64}, udata::Vector{Float64}, ydata::Vector{Float64})
    logout = engee.run(model, verbose=false)
    out_plot = plot(
        logout[output_name].time, 
        logout[output_name].value, 
        label="моделирование", 
        title = "Выход", 
        xlabel="t, с", 
        legend=:bottomright,
        linewidth=3
        )
    plot!(out_plot, tdata, ydata, label="эксперимент")
    in_plot = plot(tdata, udata, title = "Вход", xlabel="t, c", legend = false)
    p = plot(out_plot,in_plot,layout = (2, 1), plot_title="Эксперимент и моделирование")
end

# график невязок
function residual_plot(model, output_name::String, tdata::Vector{Float64}, ydata::Vector{Float64})
    logout = engee.run(model, verbose=false)
    nearest = interpolate((logout[output_name].time,), logout[output_name].value, Gridded(Constant()))
    residals = ydata .- nearest.(tdata)
    reside = plot(tdata, residals, title = "Ошибки", xlabel="t, с", legend = false)
end

# График измения переменых
function variable_plot(p_names::Vector{String}, parametrs_values)
    p_trace = plot(title = "Значения параметров", xlabel="Количество оценок целевой функции",)
    parametrs_values_m = mapreduce(permutedims, vcat, parametrs_values)
    for i in 1:n
        plot!(p_trace, 1:length(parametrs_values_m[:,i]), parametrs_values_m[:,i], markers=:auto, label = p_names[i])
    end
    return p_trace
end

n = parametrs_number

# Векторы для логирования значений целевой функции и переменных
loss_function_vector = Vector{Float64}()
parametrs_values = Vector{Vector{Float64}}()

#функция потерь
function myfunc(x::Vector, grad::Vector, output_name::String, measure_time, measure_value, parametrs_name...)
    str = ""
    iterations_value = Vector{Float64}()
    for (value,p) in zip(x, parametrs_name)
        global parametr = Symbol(p)
        @eval(($parametr)=$(value))
        str = str*"$p = $value, "
        push!(iterations_value, value)
    end
    push!(parametrs_values, iterations_value)
    simulation_out = engee.run(model, verbose=false) |> x->collect(x[output_name])
    if measure_time[end]>simulation_out.time[end]  
        push!(simulation_out,[measure_time[end], simulation_out.time[end]])
    end
    nearest = interpolate(
        (simulation_out.time,), 
        simulation_out.value, 
        Gridded(Constant())
    )
    l = sum(abs2, measure_value .- nearest.(measure_time))
    str = "$(length(parametrs_values)). "*str*"loss = $l"
    println(str)
    push!(loss_function_vector, l)
    return l
end


opt = Opt(algoritm, n) # алгоритм без градиента
algoritm === :G_MLSL && NLopt.local_optimizer!(opt, Opt(local_algoritm, n))
opt.lower_bounds = p_lower_bounds
opt.upper_bounds = p_upper_bounds
opt.xtol_rel = xtol_rel
opt.xtol_abs = xtol_abs
opt.ftol_rel = ftol_rel
opt.ftol_abs = ftol_rel
opt.maxeval =  maxeval

opt.min_objective = (x,g) -> myfunc(x,g,output_name,output_data[:,1],output_data[:,2],p_names...)

(minf,minx,ret) = optimize(opt,p_0)
numevals = opt.numevals 

str = "\nРезультаты оценки:\n"
for (name, value) in zip(p_names, minx) 
    str = str*"$name = $value\n"
end
str = str*"\nЗначение целевой функции = $minf.\n\nОптимизация завершена после $numevals оценок целевой функции cо статусом: $ret.\n"
print(str)

compare && display(compare_plot(model, output_name, input_data[:,1], input_data[:,2], output_data[:,2]))
println("")
reside && display(residual_plot(model, output_name, output_data[:,1], output_data[:,2]))
println("")
objective &&
    display(
        plot(1:numevals, loss_function_vector, title = "Целевая функция", xlabel="Количество оценок целевой функции", legend = false, markers = :circle)
    )
println("")   
variables && display(variable_plot(p_names, parametrs_values))
println("")
1. b = 0.01, c = 0.7, loss = 782827.2305232552
2. b = 0.6269504073816847, c = 3.165466261913612, loss = 2.0824007980727735e6
3. b = 0.19966846490239398, c = 2.6384859605251734, loss = 1.5577384910763318e6
4. b = 0.43616990062596706, c = 5.260365036287981, loss = 2.1092703100347244e6
5. b = 0.13062205726902043, c = 0.10527521129684247, loss = 491882.1601205184
6. b = 0.13062205726902043, c = 0.10527521129684247, loss = 491882.1601205184
7. b = 0.22858860022078575, c = 0.10527521129684247, loss = 994688.4244879746
8. b = 0.13062205726902043, c = 0.18423161976947433, loss = 511161.069044274
9. b = 0.03272744808240044, c = 0.10225003874684467, loss = 153347.19109891905
10. b = 0.0, c = 0.02782974568791464, loss = 3.8266403721268633e6
11. b = 0.02681806322965815, c = 0.14143990273494417, loss = 290065.37337741273
12. b = 0.0571202535648882, c = 0.10402153357535857, loss = 11104.510447718792
13. b = 0.05772050887137111, c = 0.10013441411262017, loss = 12468.798951365312
14. b = 0.08125046811420773, c = 0.10740009755882907, loss = 140047.86862450984
15. b = 0.05638690894821858, c = 0.1081700517083551, loss = 9544.510125887842
16. b = 0.08042415294727309, c = 0.11195469496423044, loss = 135148.34762457936
17. b = 0.05502463241104411, c = 0.11520759135488587, loss = 7055.968949365142
18. b = 0.030655512945794418, c = 0.11323568367800015, loss = 194849.15061134959
19. b = 0.042840072678419265, c = 0.114221637516443, loss = 27108.42978479694
20. b = 0.06663619406442059, c = 0.11834270535828667, loss = 50582.71046448686
21. b = 0.060830413237732354, c = 0.11677514835658626, loss = 23836.651815294274
22. b = 0.05196789447732566, c = 0.11507068215568887, loss = 3723.6422274577717
23. b = 0.05162219593181882, c = 0.11261907551667362, loss = 3632.4460192722363
24. b = 0.04857207270227998, c = 0.11283116783827159, loss = 5821.098745018263
25. b = 0.053134632876166016, c = 0.11242893153633476, loss = 4431.646369650236
26. b = 0.05085804720915753, c = 0.11265381755660105, loss = 3679.943828584574
27. b = 0.05160314914411082, c = 0.11241079505360786, loss = 3629.787082033925
28. b = 0.052347127340599924, c = 0.11226599714163608, loss = 3870.2546740432335
29. b = 0.051975138242355376, c = 0.11233839609762197, loss = 3714.5084536922413
30. b = 0.05122061961067981, c = 0.1124194949597824, loss = 3618.8228191219728

Результаты оценки:
b = 0.05122061961067981
c = 0.1124194949597824

Значение целевой функции = 3618.8228191219728.

Оптимизация завершена после 30 оценок целевой функции cо статусом: MAXEVAL_REACHED.




Результат:

За 30 итераций у нас подобрались следующие параметры:

In [ ]:
print("Результаты оценки: b = ", b, ", c = ", c)
Результаты оценки: b = 0.05122061961067981, c = 0.1124194949597824

Результаты нас вполне устраивают.

Чтобы улучшить результат, можно экспериментировать с начальными предположениями, настройками оптимизации, а также подавать различные входные данные на экспериментальный стенд.