Документация 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 [ ]:
df_in = DataFrame(time=exp_time_data, value=exp_in_data)
df_in = Float64.(df_in);
wa_in = WorkspaceArray(string(rand()), df_in)
Out[0]:
WorkspaceArray{Float64}("0.776689036071537")

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

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

Сравнение экспериментальных данных и данных модели
In [ ]:
model_name = "parameter_estimation" # @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
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
Warning: attempting to remove probably stale pidfile
  path = "/user/.packages/compiled/v1.12/NLopt/faRdv_P8JK9.ji.pidfile"
@ FileWatching.Pidfile /usr/local/julia/share/julia/stdlib/v1.12/FileWatching/src/pidfile.jl:247
[ Info: Precompiling NLopt [76087f3c-5699-56af-9a33-bf431cd00edd] (cache misses: wrong julia version (2))

SYSTEM: caught exception of type :MethodError while trying to print a failed Task notice; giving up
GKS: cannot open display - headless operation mode active
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
   Resolving package versions...
   Installed LineSearch ─────────────── v0.1.7
   Installed DiffEqCallbacks ────────── v4.14.0
   Installed ControlSystems ─────────── v1.15.2
   Installed DifferentiationInterface ─ v0.7.16
    Updating `~/.project/Project.toml`
 [a98d9a8b] + Interpolations v0.15.1
    Updating `~/.project/Manifest.toml`
  [13072b0f] + AxisAlgorithms v1.1.0
 [a98d9a8b] + Interpolations v0.15.1
  [6fe1bfb0] + OffsetArrays v1.17.0
  [c84ed2f1] + Ratios v0.4.5
  [efce3f68] + WoodburyMatrices v1.1.0
        Info Packages marked with  have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated -m`
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.018087503815033434, c = 1.6015345394854297, loss = 10751.151224272222
3. b = 0.866082233489766, c = 4.0722066078109895, loss = 2.2261803894698005e6
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
4. b = 0.3162940889231156, c = 2.436950692944041, loss = 1.7393234184763955e6
5. b = 0.6520248188926385, c = 0.5744586910483873, loss = 1.7806868536939798e6
6. b = 0.018087503815033434, c = 1.6015345394854297, loss = 10751.151224272222
7. b = 0.03165313167630851, c = 1.6015345394854297, loss = 41093.85357200353
8. b = 0.018087503815033434, c = 2.802685444099502, loss = 256788.70855000004
9. b = 0.016427095185229786, c = 0.4094150027720766, loss = 590410.1647736904
10. b = 0.01725729950013161, c = 1.005474771128753, loss = 185323.31124133876
11. b = 0.017799113771523295, c = 1.9007345996891476, loss = 2339.5281199926985
12. b = 0.016876309227326317, c = 1.8956857264241798, loss = 776.3044984104805
13. b = 0.014777143064714567, c = 2.1315370069861075, loss = 8095.828968167254
14. b = 0.018339508283451707, c = 1.971568186106855, loss = 8960.157076689598
15. b = 0.016463902154722154, c = 1.8300932490469881, loss = 1265.681794168338
16. b = 0.016159324324936164, c = 1.9357545861408532, loss = 856.9200125547208
17. b = 0.017157302475527644, c = 1.9237913353787315, loss = 1980.0248528142763
18. b = 0.016769126425534724, c = 1.8794940554039552, loss = 522.5866018855496
19. b = 0.016670572976754266, c = 1.8628781264788314, loss = 517.7273584417683
20. b = 0.01673318313102868, c = 1.8610367137619854, loss = 510.90152989053706
21. b = 0.016896443710708992, c = 1.8490671676753345, loss = 532.9584938798203
22. b = 0.01662888667192703, c = 1.8593700209089208, loss = 557.5289774965057
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
23. b = 0.016784060300742858, c = 1.8597247976362825, loss = 505.31482464295294
24. b = 0.01680559641094343, c = 1.8640118252947393, loss = 490.658135054997
25. b = 0.01683818165159883, c = 1.8677118715051393, loss = 491.4433858887315
26. b = 0.01682188903127113, c = 1.8658618483999394, loss = 489.1417479288798
27. b = 0.016844694598450387, c = 1.8646676220612934, loss = 489.2103388894403
28. b = 0.01683434684998047, c = 1.8679323462990034, loss = 491.47310317880596
29. b = 0.016814813657318146, c = 1.8648701581503473, loss = 489.43293652011465
30. b = 0.016824573579231453, c = 1.866398020407165, loss = 489.3568382001504

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

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

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




Результат:

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

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

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

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